mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 20:14:16 +08:00
153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
"""preferences mtime 缓存测试 — 读盘去重, 且外部修改/自身写入后立即可见。"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from app.services import preferences
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolated(tmp_path, monkeypatch):
|
|
path = tmp_path / "preferences.json"
|
|
monkeypatch.setattr(preferences, "_path", lambda: path)
|
|
preferences._invalidate_cache()
|
|
yield path
|
|
preferences._invalidate_cache()
|
|
|
|
|
|
def _patched_loads(monkeypatch, counter: dict):
|
|
real_loads = json.loads
|
|
|
|
def _counting(text):
|
|
counter["loads"] += 1
|
|
return real_loads(text)
|
|
|
|
monkeypatch.setattr(preferences.json, "loads", _counting)
|
|
|
|
|
|
def test_second_load_hits_cache_without_disk_parse(_isolated, monkeypatch):
|
|
_isolated.write_text(json.dumps({"realtime_quotes_enabled": True}), encoding="utf-8")
|
|
counter = {"loads": 0}
|
|
_patched_loads(monkeypatch, counter)
|
|
|
|
assert preferences.load()["realtime_quotes_enabled"] is True
|
|
assert preferences.load()["realtime_quotes_enabled"] is True
|
|
assert counter["loads"] == 1, "签名未变时第二次 load 不得重复读盘+parse"
|
|
|
|
|
|
def test_external_file_change_invalidates_cache(_isolated, monkeypatch):
|
|
_isolated.write_text(json.dumps({"realtime_quote_interval": 6.0}), encoding="utf-8")
|
|
assert preferences.load()["realtime_quote_interval"] == 6.0
|
|
|
|
_isolated.write_text(json.dumps({"realtime_quote_interval": 3.0}), encoding="utf-8")
|
|
# 同尺寸修改且 mtime 粒度可能不变时, 显式推进 mtime 模拟真实场景
|
|
st = _isolated.stat()
|
|
os.utime(_isolated, ns=(st.st_atime_ns, st.st_mtime_ns + 1_000_000))
|
|
|
|
assert preferences.load()["realtime_quote_interval"] == 3.0
|
|
|
|
|
|
def test_save_then_load_sees_merged_values(_isolated):
|
|
_isolated.write_text(json.dumps({"a": 1}), encoding="utf-8")
|
|
out = preferences.save({"b": 2})
|
|
assert out == {"a": 1, "b": 2}
|
|
assert preferences.load() == {"a": 1, "b": 2}
|
|
|
|
|
|
def test_interval_setter_invalidates_cache(_isolated):
|
|
preferences.set_realtime_quote_interval(2.0)
|
|
assert preferences.load()["realtime_quote_interval"] == 2.0
|
|
|
|
|
|
def test_load_returns_copy_not_cached_object(_isolated):
|
|
_isolated.write_text(json.dumps({"k": [1, 2]}), encoding="utf-8")
|
|
first = preferences.load()
|
|
first["k"].append(3)
|
|
first["extra"] = True
|
|
again = preferences.load()
|
|
assert again == {"k": [1, 2]}
|
|
|
|
|
|
def test_mining_schedule_defaults_are_disabled(_isolated):
|
|
assert preferences.get_mining_schedule() == {
|
|
"mining_schedule_enabled": False,
|
|
"mining_schedule_weekday": 4,
|
|
"mining_budget_profile": "balanced",
|
|
}
|
|
|
|
|
|
def test_mining_schedule_invalid_stored_values_fail_closed(_isolated):
|
|
_isolated.write_text(
|
|
json.dumps(
|
|
{
|
|
"mining_schedule_enabled": "false",
|
|
"mining_schedule_weekday": True,
|
|
"mining_budget_profile": None,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert preferences.get_mining_schedule() == {
|
|
"mining_schedule_enabled": False,
|
|
"mining_schedule_weekday": 4,
|
|
"mining_budget_profile": "balanced",
|
|
}
|
|
|
|
|
|
def test_mining_schedule_setter_saves_group_once(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(preferences, "save", lambda updates: calls.append(updates) or updates)
|
|
|
|
result = preferences.set_mining_schedule(True, 2, "strict")
|
|
|
|
assert result == {
|
|
"mining_schedule_enabled": True,
|
|
"mining_schedule_weekday": 2,
|
|
"mining_budget_profile": "strict",
|
|
}
|
|
assert calls == [result]
|
|
|
|
|
|
@pytest.mark.parametrize("weekday", [-1, 5, True])
|
|
def test_mining_schedule_setter_rejects_invalid_weekday(weekday):
|
|
with pytest.raises(ValueError, match="weekday"):
|
|
preferences.set_mining_schedule(True, weekday, "balanced")
|
|
|
|
|
|
def test_mining_schedule_setter_rejects_invalid_profile():
|
|
with pytest.raises(ValueError, match="profile"):
|
|
preferences.set_mining_schedule(True, 4, "exploratory")
|
|
|
|
|
|
def test_external_push_channel_whitelists_include_custom_and_email(_isolated):
|
|
assert preferences.set_webhook_default_channels([
|
|
"custom", "email", "custom", "unsupported",
|
|
]) == ["custom", "email"]
|
|
assert preferences.set_review_push_channels([
|
|
"email", "wecom", "unsupported",
|
|
]) == ["email", "wecom"]
|
|
|
|
|
|
def test_email_smtp_config_falls_back_from_malformed_stored_values(_isolated):
|
|
preferences.save({
|
|
"email_smtp_config": {
|
|
"host": " smtp.example.com ",
|
|
"port": "invalid",
|
|
"security": "invalid",
|
|
"to_addresses": "alerts@example.com",
|
|
},
|
|
})
|
|
|
|
assert preferences.get_email_smtp_config() == {
|
|
"host": "smtp.example.com",
|
|
"port": 465,
|
|
"security": "ssl",
|
|
"username": "",
|
|
"from_address": "",
|
|
"to_addresses": [],
|
|
}
|