Files
NotesAgentic/backend/tests/test_pdf_theme_resources.py
T
admin d703ab64e3
CI / docs-check (push) Canceled after 0s
CI / backend-test (push) Canceled after 0s
CI / service-test (push) Canceled after 0s
CI / frontend-test (push) Canceled after 0s
CI / rust-core (push) Canceled after 0s
CI / docs-check (pull_request) Canceled after 0s
CI / backend-test (pull_request) Canceled after 0s
CI / service-test (pull_request) Canceled after 0s
CI / frontend-test (pull_request) Canceled after 0s
CI / rust-core (pull_request) Canceled after 0s
docs: 将仓库代码注释统一为中文
2026-09-10 00:40:56 +08:00

97 lines
4.9 KiB
Python

"""PDF主题和资源政策回归;没有真正的提供者或用户文件。"""
import asyncio
import base64
from io import BytesIO
import pytest
from PIL import Image
from pydantic import ValidationError
from app.contracts import ExportAsset, ExportOptions, ExportRequest
from app.export.assets import validate_assets, enrich_document, source_hash
from app.export.exporters.pdf import PdfExporter
from app.export.markdown import parse_document
from app.export.themes import PALETTES
from app.export import service
def png_asset(size=(40,30), source='graph LR; A-->B'):
out=BytesIO(); Image.new('RGBA',size,(0,0,0,0)).save(out,'PNG')
return ExportAsset(kind='mermaid',source_hash=source_hash(source),png_base64=base64.b64encode(out.getvalue()).decode())
@pytest.mark.parametrize('theme',list(PALETTES))
def test_pdf_theme_colors_are_written_on_every_page(theme):
import re, zlib
palette=PALETTES[theme]
doc=parse_document(('## Section\n\nText body\n\n> Quoted text\n\n```python\nprint(1)\n```\n\n')*30)
result=PdfExporter().render(doc,ExportOptions(theme_id=theme))
streams=[]
for match in re.finditer(rb'stream\r?\n(.*?)endstream',result.content,re.S):
try: streams.append(zlib.decompress(base64.a85decode(match[1].strip().removesuffix(b'~>'))))
except Exception: pass
from reportlab.lib.rl_accel import fp_str
command=(fp_str(*[int(palette[0][i:i+2],16)/255 for i in (1,3,5)])+' rg').encode()
pages=[s for s in streams if b'BT' in s and b'/F' in s]
assert len(pages)>1
assert all(command in s for s in pages)
assert not any('浅色打印' in w for w in result.warnings)
def test_pdf_accepts_asset_contract_beyond_previous_count_and_size():
assets=[png_asset(source=str(i)) for i in range(65)]
assets[0]=assets[0].model_copy(update={'png_base64':'A'*2800004})
values=dict(source={'type':'markdown','markdown':'content'},assets=assets)
ExportRequest(format='pdf',**values)
with pytest.raises(ValidationError): ExportRequest(format='html',**values)
with pytest.raises(ValidationError): ExportRequest(format='docx',**values)
def test_pdf_large_png_still_requires_valid_format():
asset=png_asset((2100,2000))
assert validate_assets([asset],unlimited=True)
with pytest.raises(Exception): validate_assets([asset])
with pytest.raises(Exception): validate_assets([asset.model_copy(update={'png_base64':'invalid'})],unlimited=True)
def test_pdf_embeds_more_than_64_resources_with_theme_background():
doc=parse_document(('```mermaid\ngraph LR; A-->B\n```\n\n')*65)
from app.export.assets import attach_assets
attach_assets(doc,validate_assets([png_asset()],unlimited=True))
assert not enrich_document(doc,unlimited=True,options=ExportOptions(theme_id='dark'))
assert all('static_png' in node.attributes for node in doc.children)
with Image.open(BytesIO(doc.children[-1].attributes['static_png'])) as image:
assert image.getpixel((0,0)) == (13,17,23)
assert PdfExporter().render(doc,ExportOptions(theme_id='dark')).content.startswith(b'%PDF')
def test_pdf_pipeline_ignores_source_and_output_quotas(monkeypatch):
monkeypatch.setattr(service,'MAX_MARKDOWN_CHARS',8)
monkeypatch.setattr(service,'MAX_EXPORT_BYTES',8)
async def run():
job=await service.create_export(ExportRequest(format='pdf',source={'type':'markdown','markdown':'Beyond the previous quota.'}))
done=await service.wait_for_export(job.job_id)
assert done.status.value=='completed'
assert service.get_export_file(job.job_id).stat().st_size>8
with pytest.raises(Exception):
await service.create_export(ExportRequest(format='html',source={'type':'markdown','markdown':'Beyond the previous quota.'}))
asyncio.run(run())
def test_pdf_accepts_more_than_16_curves_and_keeps_expression_safety():
doc=parse_document('```function-plot\n'+'\n'.join(f'y=x+{i}' for i in range(17))+'\n```')
result=PdfExporter().render(doc,ExportOptions(theme_id='dark'))
assert not any('函数图像' in w for w in result.warnings)
unsafe=PdfExporter().render(parse_document('```function-plot\ny=__import__("os")\n```'),ExportOptions())
assert any('函数图像' in w for w in unsafe.warnings)
def test_pdf_custom_palette_and_math_color():
palette=dict(zip(('page','surface','text','muted','code','border','accent'),PALETTES['midnight-purple']))
options=ExportOptions(theme_id='my-theme',palette=palette)
doc=parse_document('Formula $x^2$')
assert not enrich_document(doc,unlimited=True,options=options)
math=next(n for n in doc.children[0].children if n.type=='math_inline')
with Image.open(BytesIO(math.attributes['static_png'])) as image:
assert image.getpixel((0,0))==(25,19,34)
assert not any('主题' in w for w in PdfExporter().render(doc,options).warnings)
with pytest.raises(ValidationError): ExportOptions(palette={**palette,'text':'url(file:///private)'})