Files
iAOP/templates/ti-cl4/perf-budget/tests/test_lazy_load.py
T
bot_dev1 5da367dfd8 feat(#54): 性能预算与懒加载策略引擎(首屏≤2s,图表懒加载)
纯标准库实现,对齐 PRD 5.5「⑤ 配置化驾驶舱」性能口径:

- perf_budget.py:PerformanceBudget(首屏 2000ms + 6 类资源预算)、
  ResourceMeasurement(block_render 区分关键路径)、BudgetVerifier
  (阻塞串行+非阻塞取最大口径,逐类型比对预算,PASS/FAIL/EMPTY +
  类型化优化建议,reason 可解释)。
- lazy_load.py:LazyLoadStrategy(immediate/visible/idle/never)、
  LazyLoadPolicy(视口行数+预加载+分页阈值+虚拟滚动)、LazyLoadPlanner
  (widget 坐标+滚动位置 → 决策+分页计划+虚拟滚动窗口)。
- tests:32 用例覆盖预算构造/查询、关键路径口径、达标/超预算场景、
  always_load 强制加载、视口边界、分页/虚拟滚动、输入校验。
- _sanity_check.py:冒烟验证达标/超预算/懒加载分流。
2026-08-05 05:26:09 +08:00

186 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()