|
54d5d79…
|
noreply
|
1 |
"""Tests for PDF and PPTX exporters.""" |
|
54d5d79…
|
noreply
|
2 |
|
|
54d5d79…
|
noreply
|
3 |
import pytest |
|
54d5d79…
|
noreply
|
4 |
|
|
54d5d79…
|
noreply
|
5 |
from video_processor.exporters.pdf_export import generate_pdf |
|
54d5d79…
|
noreply
|
6 |
from video_processor.exporters.pptx_export import generate_pptx |
|
54d5d79…
|
noreply
|
7 |
|
|
54d5d79…
|
noreply
|
8 |
|
|
54d5d79…
|
noreply
|
9 |
def _sample_kg(): |
|
54d5d79…
|
noreply
|
10 |
"""Return a sample knowledge graph dict for testing.""" |
|
54d5d79…
|
noreply
|
11 |
return { |
|
54d5d79…
|
noreply
|
12 |
"nodes": [ |
|
54d5d79…
|
noreply
|
13 |
{"name": "Python", "type": "technology", "descriptions": ["A programming language"]}, |
|
54d5d79…
|
noreply
|
14 |
{"name": "Django", "type": "technology", "descriptions": ["A web framework"]}, |
|
54d5d79…
|
noreply
|
15 |
{"name": "Alice", "type": "person", "descriptions": ["Software engineer"]}, |
|
54d5d79…
|
noreply
|
16 |
{"name": "Bob", "type": "person", "descriptions": ["Product manager"]}, |
|
54d5d79…
|
noreply
|
17 |
{"name": "Acme Corp", "type": "organization", "descriptions": ["A tech company"]}, |
|
54d5d79…
|
noreply
|
18 |
], |
|
54d5d79…
|
noreply
|
19 |
"relationships": [ |
|
54d5d79…
|
noreply
|
20 |
{"source": "Alice", "target": "Python", "type": "uses"}, |
|
54d5d79…
|
noreply
|
21 |
{"source": "Alice", "target": "Bob", "type": "works_with"}, |
|
54d5d79…
|
noreply
|
22 |
{"source": "Django", "target": "Python", "type": "built_on"}, |
|
54d5d79…
|
noreply
|
23 |
{"source": "Alice", "target": "Acme Corp", "type": "employed_by"}, |
|
54d5d79…
|
noreply
|
24 |
], |
|
54d5d79…
|
noreply
|
25 |
} |
|
54d5d79…
|
noreply
|
26 |
|
|
54d5d79…
|
noreply
|
27 |
|
|
54d5d79…
|
noreply
|
28 |
def _empty_kg(): |
|
54d5d79…
|
noreply
|
29 |
return {"nodes": [], "relationships": []} |
|
54d5d79…
|
noreply
|
30 |
|
|
54d5d79…
|
noreply
|
31 |
|
|
54d5d79…
|
noreply
|
32 |
class TestPDFExport: |
|
54d5d79…
|
noreply
|
33 |
@pytest.fixture(autouse=True) |
|
54d5d79…
|
noreply
|
34 |
def _check_reportlab(self): |
|
54d5d79…
|
noreply
|
35 |
pytest.importorskip("reportlab") |
|
54d5d79…
|
noreply
|
36 |
|
|
54d5d79…
|
noreply
|
37 |
def test_generate_pdf(self, tmp_path): |
|
54d5d79…
|
noreply
|
38 |
out = tmp_path / "report.pdf" |
|
54d5d79…
|
noreply
|
39 |
result = generate_pdf(_sample_kg(), out, title="Test Report") |
|
54d5d79…
|
noreply
|
40 |
assert result == out |
|
54d5d79…
|
noreply
|
41 |
assert out.exists() |
|
54d5d79…
|
noreply
|
42 |
assert out.stat().st_size > 0 |
|
54d5d79…
|
noreply
|
43 |
|
|
54d5d79…
|
noreply
|
44 |
def test_generate_pdf_empty_kg(self, tmp_path): |
|
54d5d79…
|
noreply
|
45 |
out = tmp_path / "empty.pdf" |
|
54d5d79…
|
noreply
|
46 |
result = generate_pdf(_empty_kg(), out) |
|
54d5d79…
|
noreply
|
47 |
assert result == out |
|
54d5d79…
|
noreply
|
48 |
assert out.exists() |
|
54d5d79…
|
noreply
|
49 |
|
|
54d5d79…
|
noreply
|
50 |
def test_generate_pdf_creates_parent_dirs(self, tmp_path): |
|
54d5d79…
|
noreply
|
51 |
out = tmp_path / "sub" / "dir" / "report.pdf" |
|
54d5d79…
|
noreply
|
52 |
result = generate_pdf(_sample_kg(), out) |
|
54d5d79…
|
noreply
|
53 |
assert result == out |
|
54d5d79…
|
noreply
|
54 |
assert out.exists() |
|
54d5d79…
|
noreply
|
55 |
|
|
54d5d79…
|
noreply
|
56 |
def test_generate_pdf_default_title(self, tmp_path): |
|
54d5d79…
|
noreply
|
57 |
out = tmp_path / "default.pdf" |
|
54d5d79…
|
noreply
|
58 |
generate_pdf(_sample_kg(), out) |
|
54d5d79…
|
noreply
|
59 |
assert out.exists() |
|
54d5d79…
|
noreply
|
60 |
|
|
54d5d79…
|
noreply
|
61 |
def test_generate_pdf_with_diagrams_dir(self, tmp_path): |
|
54d5d79…
|
noreply
|
62 |
diag_dir = tmp_path / "diagrams" |
|
54d5d79…
|
noreply
|
63 |
diag_dir.mkdir() |
|
54d5d79…
|
noreply
|
64 |
out = tmp_path / "report.pdf" |
|
54d5d79…
|
noreply
|
65 |
# No PNGs in dir — should still work |
|
54d5d79…
|
noreply
|
66 |
result = generate_pdf(_sample_kg(), out, diagrams_dir=diag_dir) |
|
54d5d79…
|
noreply
|
67 |
assert result == out |
|
54d5d79…
|
noreply
|
68 |
|
|
54d5d79…
|
noreply
|
69 |
def test_generate_pdf_no_reportlab(self, tmp_path, monkeypatch): |
|
54d5d79…
|
noreply
|
70 |
"""Verify ImportError propagates when reportlab is missing.""" |
|
54d5d79…
|
noreply
|
71 |
import builtins |
|
54d5d79…
|
noreply
|
72 |
|
|
54d5d79…
|
noreply
|
73 |
real_import = builtins.__import__ |
|
54d5d79…
|
noreply
|
74 |
|
|
54d5d79…
|
noreply
|
75 |
def mock_import(name, *args, **kwargs): |
|
54d5d79…
|
noreply
|
76 |
if name.startswith("reportlab"): |
|
54d5d79…
|
noreply
|
77 |
raise ImportError("No module named 'reportlab'") |
|
54d5d79…
|
noreply
|
78 |
return real_import(name, *args, **kwargs) |
|
54d5d79…
|
noreply
|
79 |
|
|
54d5d79…
|
noreply
|
80 |
monkeypatch.setattr(builtins, "__import__", mock_import) |
|
54d5d79…
|
noreply
|
81 |
with pytest.raises(ImportError): |
|
54d5d79…
|
noreply
|
82 |
generate_pdf(_sample_kg(), tmp_path / "fail.pdf") |
|
54d5d79…
|
noreply
|
83 |
|
|
54d5d79…
|
noreply
|
84 |
|
|
54d5d79…
|
noreply
|
85 |
class TestPPTXExport: |
|
54d5d79…
|
noreply
|
86 |
@pytest.fixture(autouse=True) |
|
54d5d79…
|
noreply
|
87 |
def _check_pptx(self): |
|
54d5d79…
|
noreply
|
88 |
pytest.importorskip("pptx") |
|
54d5d79…
|
noreply
|
89 |
|
|
54d5d79…
|
noreply
|
90 |
def test_generate_pptx(self, tmp_path): |
|
54d5d79…
|
noreply
|
91 |
out = tmp_path / "slides.pptx" |
|
54d5d79…
|
noreply
|
92 |
result = generate_pptx(_sample_kg(), out, title="Test Deck") |
|
54d5d79…
|
noreply
|
93 |
assert result == out |
|
54d5d79…
|
noreply
|
94 |
assert out.exists() |
|
54d5d79…
|
noreply
|
95 |
assert out.stat().st_size > 0 |
|
54d5d79…
|
noreply
|
96 |
|
|
54d5d79…
|
noreply
|
97 |
def test_generate_pptx_empty_kg(self, tmp_path): |
|
54d5d79…
|
noreply
|
98 |
out = tmp_path / "empty.pptx" |
|
54d5d79…
|
noreply
|
99 |
result = generate_pptx(_empty_kg(), out) |
|
54d5d79…
|
noreply
|
100 |
assert result == out |
|
54d5d79…
|
noreply
|
101 |
assert out.exists() |
|
54d5d79…
|
noreply
|
102 |
|
|
54d5d79…
|
noreply
|
103 |
def test_generate_pptx_creates_parent_dirs(self, tmp_path): |
|
54d5d79…
|
noreply
|
104 |
out = tmp_path / "sub" / "dir" / "slides.pptx" |
|
54d5d79…
|
noreply
|
105 |
result = generate_pptx(_sample_kg(), out) |
|
54d5d79…
|
noreply
|
106 |
assert result == out |
|
54d5d79…
|
noreply
|
107 |
assert out.exists() |
|
54d5d79…
|
noreply
|
108 |
|
|
54d5d79…
|
noreply
|
109 |
def test_generate_pptx_with_diagrams_dir(self, tmp_path): |
|
54d5d79…
|
noreply
|
110 |
diag_dir = tmp_path / "diagrams" |
|
54d5d79…
|
noreply
|
111 |
diag_dir.mkdir() |
|
54d5d79…
|
noreply
|
112 |
out = tmp_path / "slides.pptx" |
|
54d5d79…
|
noreply
|
113 |
result = generate_pptx(_sample_kg(), out, diagrams_dir=diag_dir) |
|
54d5d79…
|
noreply
|
114 |
assert result == out |
|
54d5d79…
|
noreply
|
115 |
|
|
54d5d79…
|
noreply
|
116 |
def test_pptx_slide_count(self, tmp_path): |
|
54d5d79…
|
noreply
|
117 |
"""Verify expected number of slides are created.""" |
|
54d5d79…
|
noreply
|
118 |
from pptx import Presentation |
|
54d5d79…
|
noreply
|
119 |
|
|
54d5d79…
|
noreply
|
120 |
out = tmp_path / "count.pptx" |
|
54d5d79…
|
noreply
|
121 |
generate_pptx(_sample_kg(), out) |
|
54d5d79…
|
noreply
|
122 |
prs = Presentation(str(out)) |
|
54d5d79…
|
noreply
|
123 |
# Title + Overview + Key Entities + Rel Types + 1 entity batch = 5 |
|
54d5d79…
|
noreply
|
124 |
assert len(prs.slides) == 5 |
|
54d5d79…
|
noreply
|
125 |
|
|
54d5d79…
|
noreply
|
126 |
def test_pptx_many_entities_batched(self, tmp_path): |
|
54d5d79…
|
noreply
|
127 |
"""Entities are batched into multiple slides when >12.""" |
|
54d5d79…
|
noreply
|
128 |
from pptx import Presentation |
|
54d5d79…
|
noreply
|
129 |
|
|
54d5d79…
|
noreply
|
130 |
kg = { |
|
54d5d79…
|
noreply
|
131 |
"nodes": [ |
|
54d5d79…
|
noreply
|
132 |
{"name": f"Entity{i}", "type": "concept", "descriptions": [f"desc {i}"]} |
|
54d5d79…
|
noreply
|
133 |
for i in range(25) |
|
54d5d79…
|
noreply
|
134 |
], |
|
54d5d79…
|
noreply
|
135 |
"relationships": [], |
|
54d5d79…
|
noreply
|
136 |
} |
|
54d5d79…
|
noreply
|
137 |
out = tmp_path / "many.pptx" |
|
54d5d79…
|
noreply
|
138 |
generate_pptx(kg, out) |
|
54d5d79…
|
noreply
|
139 |
prs = Presentation(str(out)) |
|
54d5d79…
|
noreply
|
140 |
# Title + Overview + 3 entity batches (12 + 12 + 1) = 5 |
|
54d5d79…
|
noreply
|
141 |
# No Key Entities or Rel Types slides (no relationships) |
|
54d5d79…
|
noreply
|
142 |
assert len(prs.slides) == 5 |
|
54d5d79…
|
noreply
|
143 |
|
|
54d5d79…
|
noreply
|
144 |
def test_generate_pptx_no_pptx(self, tmp_path, monkeypatch): |
|
54d5d79…
|
noreply
|
145 |
"""Verify ImportError propagates when python-pptx is missing.""" |
|
54d5d79…
|
noreply
|
146 |
import builtins |
|
54d5d79…
|
noreply
|
147 |
|
|
54d5d79…
|
noreply
|
148 |
real_import = builtins.__import__ |
|
54d5d79…
|
noreply
|
149 |
|
|
54d5d79…
|
noreply
|
150 |
def mock_import(name, *args, **kwargs): |
|
54d5d79…
|
noreply
|
151 |
if name.startswith("pptx"): |
|
54d5d79…
|
noreply
|
152 |
raise ImportError("No module named 'pptx'") |
|
54d5d79…
|
noreply
|
153 |
return real_import(name, *args, **kwargs) |
|
54d5d79…
|
noreply
|
154 |
|
|
54d5d79…
|
noreply
|
155 |
monkeypatch.setattr(builtins, "__import__", mock_import) |
|
54d5d79…
|
noreply
|
156 |
with pytest.raises(ImportError): |
|
54d5d79…
|
noreply
|
157 |
generate_pptx(_sample_kg(), tmp_path / "fail.pptx") |