# -*- 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 unittest 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()