Skip to content

Commit 9d8293c

Browse files
tests: Add unit tests for ExtractedTextDocument validation and serialization
- Introduced tests to validate `ExtractedTextDocument` in different modes: - Document mode with full text payload. - Page mode with structured words and page-level text. - Mode without words or full text. - Word payload variations including minimal, styled, and coordinate data. - Ensured validation of `model_validate` and `model_dump` methods. - Verified handling of optional fields like `words`, `style`, and `coordinates`. Assisted-by: Codex
1 parent 5edd8e3 commit 9d8293c

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""Tests for ExtractedTextDocument validation and serialization."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from pdfrest.models import ExtractedTextDocument
8+
9+
10+
def test_extract_text_document_round_trip_document_mode() -> None:
11+
data = {
12+
"inputId": "153ec1a0f-07e4-4f42-bc64-05180f72a06c",
13+
"fullText": "The lamb walks My Cow Eats!",
14+
}
15+
16+
document = ExtractedTextDocument.model_validate(data)
17+
18+
assert document.input_id == data["inputId"]
19+
assert document.full_text is not None
20+
assert document.full_text.document_text == "The lamb walks My Cow Eats!"
21+
assert document.full_text.iter_pages() == []
22+
assert document.words is None
23+
24+
with pytest.raises(
25+
ValueError,
26+
match="full text payload was emitted in document mode; page data unavailable",
27+
):
28+
_ = document.full_text.pages
29+
30+
assert document.model_dump(by_alias=True, exclude_none=True) == data
31+
32+
33+
def test_extract_text_document_round_trip_page_mode() -> None:
34+
data = {
35+
"inputId": "10559e808-4073-488b-b660-a0b1106dd98e",
36+
"words": [
37+
{
38+
"text": "The",
39+
"page": 1,
40+
"coordinates": {
41+
"topLeft": {"x": 72, "y": 720.7918090820312},
42+
"topRight": {"x": 90.12725830078125, "y": 720.7918090820312},
43+
"bottomLeft": {"x": 72, "y": 704.72412109375},
44+
"bottomRight": {"x": 90.12725830078125, "y": 704.72412109375},
45+
},
46+
"style": {
47+
"color": {"space": "DeviceRGB", "values": [0, 0, 0]},
48+
"font": {"name": "Calibri", "size": 12},
49+
},
50+
}
51+
],
52+
"fullText": {
53+
"pages": [
54+
{"page": 1, "text": "The lamb walks"},
55+
{"page": 2, "text": "My Cow Eats!"},
56+
]
57+
},
58+
}
59+
60+
document = ExtractedTextDocument.model_validate(data)
61+
62+
assert document.input_id == data["inputId"]
63+
assert document.full_text is not None
64+
assert document.words is not None
65+
assert len(document.words) == 1
66+
assert document.full_text.document_text == "The lamb walks My Cow Eats!"
67+
pages = document.full_text.pages
68+
assert len(pages) == 2
69+
assert pages[0].page == 1
70+
assert pages[0].text == "The lamb walks"
71+
assert pages[1].page == 2
72+
assert pages[1].text == "My Cow Eats!"
73+
assert document.full_text.iter_pages() == pages
74+
75+
word = document.words[0]
76+
assert word.text == "The"
77+
assert word.page == 1
78+
assert word.style is not None
79+
assert word.style.color.space == "DeviceRGB"
80+
assert word.style.color.values == [0, 0, 0]
81+
assert word.style.font.name == "Calibri"
82+
assert word.style.font.size == 12
83+
assert word.coordinates is not None
84+
assert word.coordinates.top_left.x == 72
85+
assert word.coordinates.top_left.y == 720.7918090820312
86+
assert word.coordinates.top_right.x == 90.12725830078125
87+
assert word.coordinates.top_right.y == 720.7918090820312
88+
assert word.coordinates.bottom_left.x == 72
89+
assert word.coordinates.bottom_left.y == 704.72412109375
90+
assert word.coordinates.bottom_right.x == 90.12725830078125
91+
assert word.coordinates.bottom_right.y == 704.72412109375
92+
93+
assert document.model_dump(by_alias=True, exclude_none=True) == data
94+
95+
96+
def test_extract_text_document_round_trip_without_words_or_full_text() -> None:
97+
data = {
98+
"inputId": "3f59e808-4073-488b-b660-a0b1106dd9aa",
99+
}
100+
101+
document = ExtractedTextDocument.model_validate(data)
102+
103+
assert document.input_id == data["inputId"]
104+
assert document.full_text is None
105+
assert document.words is None
106+
assert document.model_dump(by_alias=True, exclude_none=True) == data
107+
108+
109+
@pytest.mark.parametrize(
110+
("word_payload", "has_coordinates", "has_style"),
111+
[
112+
pytest.param(
113+
{"text": "Simple", "page": 1},
114+
False,
115+
False,
116+
id="minimal-word",
117+
),
118+
pytest.param(
119+
{
120+
"text": "CoordsOnly",
121+
"page": 2,
122+
"coordinates": {
123+
"topLeft": {"x": 1, "y": 2},
124+
"topRight": {"x": 3, "y": 4},
125+
"bottomLeft": {"x": 5, "y": 6},
126+
"bottomRight": {"x": 7, "y": 8},
127+
},
128+
},
129+
True,
130+
False,
131+
id="coordinates-only",
132+
),
133+
pytest.param(
134+
{
135+
"text": "StyleOnly",
136+
"page": 3,
137+
"style": {
138+
"color": {"space": "DeviceRGB", "values": [0.1, 0.2, 0.3]},
139+
"font": {"name": "Calibri", "size": 10},
140+
},
141+
},
142+
False,
143+
True,
144+
id="style-only",
145+
),
146+
pytest.param(
147+
{
148+
"text": "Both",
149+
"page": 4,
150+
"coordinates": {
151+
"topLeft": {"x": 10, "y": 11},
152+
"topRight": {"x": 12, "y": 13},
153+
"bottomLeft": {"x": 14, "y": 15},
154+
"bottomRight": {"x": 16, "y": 17},
155+
},
156+
"style": {
157+
"color": {"space": "DeviceCMYK", "values": [0, 0, 0, 1]},
158+
"font": {"name": "Times", "size": 8.5},
159+
},
160+
},
161+
True,
162+
True,
163+
id="coordinates-and-style",
164+
),
165+
],
166+
)
167+
def test_extracted_text_words_optional_fields(
168+
word_payload: dict[str, object], has_coordinates: bool, has_style: bool
169+
) -> None:
170+
data = {
171+
"inputId": "6f59e808-4073-488b-b660-a0b1106dd9bb",
172+
"words": [word_payload],
173+
}
174+
175+
document = ExtractedTextDocument.model_validate(data)
176+
177+
assert document.input_id == data["inputId"]
178+
assert document.words is not None
179+
word = document.words[0]
180+
assert word.text == word_payload["text"]
181+
assert word.page == word_payload["page"]
182+
183+
if has_coordinates:
184+
assert word.coordinates is not None
185+
coord_payload = word_payload.get("coordinates")
186+
assert isinstance(coord_payload, dict)
187+
top_left = coord_payload["topLeft"]
188+
top_right = coord_payload["topRight"]
189+
bottom_left = coord_payload["bottomLeft"]
190+
bottom_right = coord_payload["bottomRight"]
191+
assert word.coordinates.top_left.x == top_left["x"]
192+
assert word.coordinates.top_left.y == top_left["y"]
193+
assert word.coordinates.top_right.x == top_right["x"]
194+
assert word.coordinates.top_right.y == top_right["y"]
195+
assert word.coordinates.bottom_left.x == bottom_left["x"]
196+
assert word.coordinates.bottom_left.y == bottom_left["y"]
197+
assert word.coordinates.bottom_right.x == bottom_right["x"]
198+
assert word.coordinates.bottom_right.y == bottom_right["y"]
199+
assert word.coordinates.model_dump(by_alias=True) == coord_payload
200+
else:
201+
assert word.coordinates is None
202+
203+
if has_style:
204+
assert word.style is not None
205+
style_payload = word_payload.get("style")
206+
assert isinstance(style_payload, dict)
207+
color_payload = style_payload["color"]
208+
font_payload = style_payload["font"]
209+
assert isinstance(color_payload, dict)
210+
assert isinstance(font_payload, dict)
211+
assert word.style.color.space == color_payload["space"]
212+
assert word.style.color.values == color_payload["values"]
213+
assert word.style.font.name == font_payload["name"]
214+
assert word.style.font.size == font_payload["size"]
215+
else:
216+
assert word.style is None
217+
218+
assert document.model_dump(by_alias=True, exclude_none=True) == data
219+
220+
221+
def test_extract_text_document_page_mode_without_words() -> None:
222+
data = {
223+
"inputId": "9f59e808-4073-488b-b660-a0b1106dd9cc",
224+
"fullText": {
225+
"pages": [
226+
{"page": 1, "text": "One"},
227+
{"page": 2, "text": "Two"},
228+
]
229+
},
230+
}
231+
232+
document = ExtractedTextDocument.model_validate(data)
233+
234+
assert document.input_id == data["inputId"]
235+
assert document.words is None
236+
assert document.full_text is not None
237+
pages = document.full_text.pages
238+
assert len(pages) == 2
239+
assert pages[0].page == 1
240+
assert pages[0].text == "One"
241+
assert pages[1].page == 2
242+
assert pages[1].text == "Two"
243+
assert document.full_text.document_text == "One Two"
244+
assert document.model_dump(by_alias=True, exclude_none=True) == data

0 commit comments

Comments
 (0)