189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""图表懒加载策略引擎测试(Issue #54)。
|
||
|
||
覆盖:
|
||
1. 默认策略构造(首屏 4 行视口 + 1 行预加载);
|
||
2. 视口内 widget → IMMEDIATE;
|
||
3. always_load widget 强制 IMMEDIATE(即使视口外);
|
||
4. 视口外 widget → IDLE;
|
||
5. 分页加载(视口外超过阈值);
|
||
6. 虚拟滚动窗口计算;
|
||
7. in_viewport 边界(预加载边距);
|
||
8. 边界/错误:负 h、负 scroll_y、负视口参数;
|
||
9. 空输入。
|
||
"""
|
||
import os
|
||
import sys
|
||
import unittest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import _bootstrap # noqa: F401 (挂载 perf_budget 包)
|
||
|
||
from perf_budget.lazy_load import (
|
||
LazyLoadDecision,
|
||
LazyLoadError,
|
||
LazyLoadPlan,
|
||
LazyLoadPlanner,
|
||
LazyLoadPolicy,
|
||
LazyLoadStrategy,
|
||
WidgetLayout,
|
||
)
|
||
|
||
|
||
class TestPolicyModel(unittest.TestCase):
|
||
"""懒加载策略规则模型。"""
|
||
|
||
def test_default_cockpit_policy(self):
|
||
p = LazyLoadPolicy.default_cockpit()
|
||
self.assertEqual(p.first_viewport_rows, 4.0)
|
||
self.assertEqual(p.preload_rows, 1.0)
|
||
self.assertTrue(p.virtual_scroll)
|
||
|
||
def test_negative_viewport_rejected(self):
|
||
with self.assertRaises(LazyLoadError):
|
||
LazyLoadPolicy(first_viewport_rows=0)
|
||
|
||
def test_negative_preload_rejected(self):
|
||
with self.assertRaises(LazyLoadError):
|
||
LazyLoadPolicy(preload_rows=-1)
|
||
|
||
|
||
class TestWidgetLayoutValidation(unittest.TestCase):
|
||
"""widget 布局边界/错误。"""
|
||
|
||
def test_non_positive_h_rejected(self):
|
||
with self.assertRaises(LazyLoadError):
|
||
WidgetLayout(widget_id="w", kind="trend", y=0, h=0)
|
||
|
||
def test_negative_y_rejected(self):
|
||
with self.assertRaises(LazyLoadError):
|
||
WidgetLayout(widget_id="w", kind="trend", y=-1)
|
||
|
||
def test_empty_id_rejected(self):
|
||
with self.assertRaises(LazyLoadError):
|
||
WidgetLayout(widget_id="", kind="trend", y=0)
|
||
|
||
|
||
class TestPlannerViewport(unittest.TestCase):
|
||
"""视口判定与 immediate 决策。"""
|
||
|
||
def setUp(self):
|
||
self.planner = LazyLoadPlanner()
|
||
|
||
def test_in_viewport_widget_is_immediate(self):
|
||
# widget 在首屏视口内(y=0, h=1)→ IMMEDIATE
|
||
ws = [WidgetLayout("proc", "process_view", y=0, h=4, always_load=False)]
|
||
plan = self.planner.plan(ws, scroll_y_rows=0)
|
||
self.assertEqual(plan.decisions[0].strategy, LazyLoadStrategy.IMMEDIATE)
|
||
self.assertTrue(plan.decisions[0].in_viewport)
|
||
|
||
def test_in_viewport_boundary_with_preload(self):
|
||
# 视口 [0,4] + 预加载 1 → 视口上界 -1,下界 5
|
||
# widget y=4.5,h=1(区间 4.5~5.5)与视口 [−1,5] 相交 → 在视口
|
||
w = WidgetLayout("edge", "trend", y=4.5, h=1)
|
||
self.assertTrue(self.planner.in_viewport(w, scroll_y_rows=0))
|
||
# widget y=5.5,h=1(区间 5.5~6.5)与 [−1,5] 不相交 → 视口外
|
||
w2 = WidgetLayout("far", "trend", y=5.5, h=1)
|
||
self.assertFalse(self.planner.in_viewport(w2, scroll_y_rows=0))
|
||
|
||
|
||
class TestPlannerAlwaysLoad(unittest.TestCase):
|
||
"""always_load 强制首屏立即加载。"""
|
||
|
||
def test_always_load_forces_immediate_even_out_of_viewport(self):
|
||
planner = LazyLoadPlanner()
|
||
# widget 在视口外(y=10),但 always_load=True → IMMEDIATE
|
||
ws = [WidgetLayout("main", "process_view", y=10, h=4, always_load=True)]
|
||
plan = planner.plan(ws, scroll_y_rows=0)
|
||
self.assertEqual(plan.decisions[0].strategy, LazyLoadStrategy.IMMEDIATE)
|
||
self.assertFalse(plan.decisions[0].in_viewport)
|
||
self.assertIn("always_load", plan.decisions[0].reason)
|
||
|
||
|
||
class TestPlannerIdleAndPagination(unittest.TestCase):
|
||
"""视口外 IDLE + 分页加载。"""
|
||
|
||
def test_out_of_viewport_idle_below_threshold(self):
|
||
# 视口外 widget 数 < 阈值(6)→ IDLE 不分页
|
||
planner = LazyLoadPlanner()
|
||
ws = [WidgetLayout(f"w{i}", "trend", y=10 + i, h=1)
|
||
for i in range(3)]
|
||
plan = planner.plan(ws, scroll_y_rows=0)
|
||
for d in plan.decisions:
|
||
self.assertEqual(d.strategy, LazyLoadStrategy.IDLE)
|
||
self.assertIsNone(d.page)
|
||
|
||
def test_out_of_viewport_pagination_above_threshold(self):
|
||
# 视口外 widget 数 > 阈值(6)→ 分页(page_size=4)
|
||
planner = LazyLoadPlanner()
|
||
ws = [WidgetLayout(f"w{i}", "trend", y=10 + i, h=1)
|
||
for i in range(9)]
|
||
plan = planner.plan(ws, scroll_y_rows=0)
|
||
idle = [d for d in plan.decisions if d.strategy is LazyLoadStrategy.IDLE]
|
||
self.assertEqual(len(idle), 9)
|
||
# 9 个 widget / page_size 4 → 3 页
|
||
self.assertEqual(len(plan.pages), 3)
|
||
self.assertEqual(plan.pages[1], ["w0", "w1", "w2", "w3"])
|
||
self.assertEqual(plan.pages[3], ["w8"])
|
||
# 每个决策都有 page 号
|
||
for d in idle:
|
||
self.assertIsNotNone(d.page)
|
||
|
||
|
||
class TestVirtualScroll(unittest.TestCase):
|
||
"""虚拟滚动窗口。"""
|
||
|
||
def test_virtual_window_when_enabled(self):
|
||
planner = LazyLoadPlanner() # virtual_scroll=True 默认
|
||
plan = planner.plan(
|
||
[WidgetLayout("w", "trend", y=0, h=2)], scroll_y_rows=0)
|
||
self.assertIsNotNone(plan.virtual_window)
|
||
# scroll_y=0 → window [max(0,-1)=0, 0+4+1=5]
|
||
self.assertEqual(plan.virtual_window, (0.0, 5.0))
|
||
|
||
def test_no_virtual_window_when_disabled(self):
|
||
policy = LazyLoadPolicy(virtual_scroll=False)
|
||
planner = LazyLoadPlanner(policy)
|
||
plan = planner.plan(
|
||
[WidgetLayout("w", "trend", y=0, h=2)], scroll_y_rows=0)
|
||
self.assertIsNone(plan.virtual_window)
|
||
|
||
|
||
class TestPlanAccessorsAndErrors(unittest.TestCase):
|
||
"""计划访问器 + 输入校验。"""
|
||
|
||
def test_plan_accessors(self):
|
||
planner = LazyLoadPlanner()
|
||
ws = [
|
||
WidgetLayout("vp", "trend", y=0, h=1), # immediate
|
||
WidgetLayout("out", "trend", y=10, h=1), # idle
|
||
]
|
||
plan = planner.plan(ws, scroll_y_rows=0)
|
||
self.assertEqual(plan.immediate_ids, ["vp"])
|
||
self.assertEqual(plan.idle_ids, ["out"])
|
||
self.assertEqual(plan.never_ids, [])
|
||
|
||
def test_negative_scroll_rejected(self):
|
||
planner = LazyLoadPlanner()
|
||
with self.assertRaises(LazyLoadError):
|
||
planner.plan([WidgetLayout("w", "trend", y=0, h=1)], scroll_y_rows=-1)
|
||
|
||
def test_empty_widgets_returns_empty_plan(self):
|
||
planner = LazyLoadPlanner()
|
||
plan = planner.plan([], scroll_y_rows=0)
|
||
self.assertEqual(plan.decisions, [])
|
||
self.assertIn("无 widget", plan.reason)
|
||
|
||
def test_plan_to_dict(self):
|
||
planner = LazyLoadPlanner()
|
||
plan = planner.plan(
|
||
[WidgetLayout("w", "trend", y=0, h=1)], scroll_y_rows=0)
|
||
d = plan.to_dict()
|
||
self.assertIn("decisions", d)
|
||
self.assertIn("counts", d)
|
||
self.assertEqual(d["counts"]["immediate"], 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|