fix(plot): 修复复审问题(2 P2 + 1 P3)

- P2 复杂表达式绕过异常回退:解析与渲染共同纳入局部异常回退;
  AST 深度/节点数上限拦截 RecursionError
- P2 极端有限范围生成 nan SVG:校验坐标跨度有限且 >0,回退安全范围;
  _polyline 拒绝非有限像素坐标
- P3 更新接口契约文档:function-plot 静态 SVG 已实现

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
yxx
2026-09-05 22:49:01 +08:00
co-authored by Claude Code
parent 50d7fb4c7d
commit b87f94551b
5 changed files with 120 additions and 18 deletions
+55
View File
@@ -192,3 +192,58 @@ def test_html_exporter_function_plot_render_error_falls_back(monkeypatch) -> Non
html = result.content.decode("utf-8")
assert '<pre class="function-plot">' in html
assert any("渲染失败" in w for w in result.warnings)
# --------------------------------------------------------------------------- #
# 审阅回归:复杂表达式 / 极端数值范围
# --------------------------------------------------------------------------- #
def test_parse_expression_rejects_excessive_depth() -> None:
# P2:超长加法链的 AST 深度超限,应拒绝为 PlotParseError 而非触发 RecursionError
expr = "+".join(["1"] * 300)
with pytest.raises(PlotParseError) as exc:
parse_expression(expr)
assert exc.value.diagnostic.code == "FUNCTION_PLOT_EXPRESSION_UNSAFE"
def test_parse_expression_rejects_excessive_nodes() -> None:
# P2:浅层但节点超限的表达式(满二叉树)应被节点数上限拦截
def balanced(depth: int) -> str:
if depth == 0:
return "x"
return f"({balanced(depth - 1)}+{balanced(depth - 1)})"
expr = balanced(10) # ~2047 个节点,深度仅 ~10
with pytest.raises(PlotParseError) as exc:
parse_expression(expr)
assert exc.value.diagnostic.code == "FUNCTION_PLOT_EXPRESSION_UNSAFE"
def test_html_exporter_function_plot_deep_expression_falls_back() -> None:
# P2:复杂表达式解析失败应回退占位,不阻断整篇导出
expr = "+".join(["1"] * 300)
md = f"```function-plot\ny = {expr}\n```"
result = asyncio.run(HtmlExporter().export(parse_document(md), ExportOptions()))
html = result.content.decode("utf-8")
assert '<pre class="function-plot">' in html
assert "<svg" not in html
assert any("函数图像" in w for w in result.warnings)
def test_render_svg_extreme_domain_no_nan() -> None:
# P2:有限但跨度溢出的 domain 应回退安全范围,SVG 不得含 nan/inf
plot = parse_source("domain: -1e308, 1e308\nrange: -1, 1\ny = 0").plot
rendered = render_svg(plot)
assert "<svg" in rendered.content
assert "nan" not in rendered.content
assert "inf" not in rendered.content
assert any("domain" in w for w in rendered.warnings)
def test_render_svg_extreme_range_no_nan() -> None:
# P2:有限但跨度溢出的 range 应回退自动范围,SVG 不得含 nan/inf
plot = parse_source("domain: -1, 1\nrange: -1e308, 1e308\ny = x").plot
rendered = render_svg(plot)
assert "<svg" in rendered.content
assert "nan" not in rendered.content
assert "inf" not in rendered.content
assert any("range" in w for w in rendered.warnings)