Files
iAOP/templates/ti-cl4/perf-budget/tests/test_perf_budget.py
T

190 lines
7.5 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. 默认预算构造(首屏 2000ms + 6 类资源预算);
2. 关键路径耗时计算(阻塞串行 + 非阻塞取最大);
3. 达标场景(PASS + 裕量 reason);
4. 单类超预算(FAIL + overrun 明细 + 类型化建议);
5. 首屏总耗时超预算(FAIL + 整体建议);
6. 空测量值(EMPTY);
7. 边界/错误:负耗时、负预算、负 size、空 name。
"""
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.perf_budget import (
BudgetError,
BudgetReport,
BudgetStatus,
BudgetVerifier,
PerformanceBudget,
ResourceBudget,
ResourceMeasurement,
ResourceType,
)
from perf_budget import ( # noqa: E402 (验证顶层导出)
PerformanceBudget as _TopBudget,
BudgetVerifier as _TopVerifier,
)
class TestBudgetModel(unittest.TestCase):
"""性能预算模型构造与查询。"""
def test_default_cockpit_budget(self):
b = PerformanceBudget.default_cockpit()
self.assertEqual(b.name, "cockpit-default")
self.assertEqual(b.first_paint_ms, 2000)
# 6 类资源预算(HTML/CSS/JS/FONT/API/IMAGE)
self.assertEqual(len(b.by_type), 6)
self.assertEqual(b.budget_for(ResourceType.JS), 800)
def test_budget_for_uncovered_type_is_zero(self):
b = PerformanceBudget.default_cockpit()
# OTHER 未声明预算 → 0
self.assertEqual(b.budget_for(ResourceType.OTHER), 0.0)
def test_covered_types_ordered(self):
b = PerformanceBudget.default_cockpit()
covered = b.covered_types()
self.assertIn(ResourceType.HTML, covered)
self.assertEqual(covered, sorted(covered, key=lambda r: list(ResourceType).index(r)))
def test_negative_first_paint_rejected(self):
with self.assertRaises(BudgetError):
PerformanceBudget(name="bad", first_paint_ms=0)
def test_negative_resource_budget_rejected(self):
with self.assertRaises(BudgetError):
ResourceBudget(ResourceType.JS, -1, "x")
class TestVerifierCriticalPath(unittest.TestCase):
"""关键路径耗时口径:阻塞串行 + 非阻塞取最大。"""
def setUp(self):
self.v = BudgetVerifier(PerformanceBudget.default_cockpit())
def test_blocking_serial_accumulation(self):
# 两个阻塞 JS 各 400ms → 关键路径串行 800ms(≤ 预算)
ms = [
ResourceMeasurement("app.js", ResourceType.JS, 400, block_render=True),
ResourceMeasurement("chart.js", ResourceType.JS, 400, block_render=True),
]
report = self.v.verify(ms)
self.assertEqual(report.status, BudgetStatus.PASS)
# JS 阻塞总和 800ms(恰好等于预算,不超)
self.assertEqual(report.by_type_actual[ResourceType.JS], 800)
def test_non_blocking_takes_max(self):
# 非阻塞资源并行:取最大值(300),不累加(500)
ms = [
ResourceMeasurement("a.js", ResourceType.JS, 300, block_render=False),
ResourceMeasurement("b.js", ResourceType.JS, 200, block_render=False),
]
report = self.v.verify(ms)
# 非阻塞 JS 最大值 300ms ≤ JS 预算 800 → PASS
self.assertEqual(report.status, BudgetStatus.PASS)
self.assertEqual(report.first_paint_ms, 300)
class TestVerifierPassAndFail(unittest.TestCase):
"""达标 / 不达标场景。"""
def setUp(self):
self.v = BudgetVerifier(PerformanceBudget.default_cockpit())
def test_pass_scenario_with_margin_reason(self):
ms = [
ResourceMeasurement("doc.html", ResourceType.HTML, 150, block_render=True),
ResourceMeasurement("app.css", ResourceType.CSS, 250, block_render=True),
ResourceMeasurement("app.js", ResourceType.JS, 700, block_render=True),
ResourceMeasurement("font.woff2", ResourceType.FONT, 150, block_render=False),
]
report = self.v.verify(ms)
self.assertEqual(report.status, BudgetStatus.PASS)
self.assertTrue(report.passed)
self.assertIn("裕量", report.reason)
self.assertEqual(report.overrun, [])
def test_single_type_overrun_with_advice(self):
# JS 超预算:阻塞 950ms > 800ms 预算
ms = [
ResourceMeasurement("big.js", ResourceType.JS, 950, block_render=True),
ResourceMeasurement("app.css", ResourceType.CSS, 200, block_render=True),
ResourceMeasurement("doc.html", ResourceType.HTML, 100, block_render=True),
]
report = self.v.verify(ms)
self.assertEqual(report.status, BudgetStatus.FAIL)
self.assertEqual(len(report.overrun), 1)
ov = report.overrun[0]
self.assertEqual(ov.resource_type, ResourceType.JS)
self.assertEqual(ov.actual_ms, 950)
self.assertEqual(ov.budget_ms, 800)
self.assertGreater(ov.overrun_ms, 0)
# JS 建议含"代码分割"
self.assertIn("代码分割", ov.advice)
def test_first_paint_overrun_suggestion(self):
# 首屏总超预算但各类都没超归口预算(用未声明的 OTHER 类型撑爆总预算)
# OTHER 不在预算表,故 by_type 不报 overrun,但 first_paint 超总预算
ms = [
ResourceMeasurement("doc.html", ResourceType.HTML, 200, block_render=True),
ResourceMeasurement("app.css", ResourceType.CSS, 300, block_render=True),
ResourceMeasurement("app.js", ResourceType.JS, 800, block_render=True),
ResourceMeasurement("font.woff2", ResourceType.FONT, 200, block_render=False),
ResourceMeasurement("api", ResourceType.API, 300, block_render=False),
ResourceMeasurement("image", ResourceType.IMAGE, 100, block_render=False),
# 一个大体积 OTHER 阻塞资源把总耗时推过 2000
ResourceMeasurement("tracker.js", ResourceType.OTHER, 500, block_render=True),
]
report = self.v.verify(ms)
# 总耗时 = 2400 > 2000
self.assertGreater(report.first_paint_ms, 2000)
self.assertEqual(report.status, BudgetStatus.FAIL)
self.assertIn("首屏", report.reason)
def test_empty_measurements(self):
report = self.v.verify([])
self.assertEqual(report.status, BudgetStatus.EMPTY)
self.assertFalse(report.passed)
class TestMeasurementValidation(unittest.TestCase):
"""测量值边界/错误。"""
def test_negative_duration_rejected(self):
with self.assertRaises(BudgetError):
ResourceMeasurement("x", ResourceType.JS, -1)
def test_negative_size_rejected(self):
with self.assertRaises(BudgetError):
ResourceMeasurement("x", ResourceType.JS, 10, size_bytes=-1)
def test_empty_name_rejected(self):
with self.assertRaises(BudgetError):
ResourceMeasurement("", ResourceType.JS, 10)
class TestReportExport(unittest.TestCase):
"""报告序列化。"""
def test_to_dict_keys(self):
v = BudgetVerifier(PerformanceBudget.default_cockpit())
report = v.verify([
ResourceMeasurement("app.js", ResourceType.JS, 700, block_render=True)])
d = report.to_dict()
self.assertIn("status", d)
self.assertIn("first_paint_ms", d)
self.assertIn("by_type_actual", d)
self.assertEqual(d["passed"], True)
if __name__ == "__main__":
unittest.main()