natural-pdf 0.1.28__py3-none-any.whl → 0.1.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bad_pdf_analysis/analyze_10_more.py +300 -0
- bad_pdf_analysis/analyze_final_10.py +552 -0
- bad_pdf_analysis/analyze_specific_pages.py +394 -0
- bad_pdf_analysis/analyze_specific_pages_direct.py +382 -0
- natural_pdf/analyzers/layout/layout_analyzer.py +2 -3
- natural_pdf/analyzers/layout/layout_manager.py +44 -0
- natural_pdf/analyzers/layout/surya.py +1 -1
- natural_pdf/analyzers/shape_detection_mixin.py +228 -0
- natural_pdf/classification/manager.py +67 -0
- natural_pdf/core/element_manager.py +578 -27
- natural_pdf/core/highlighting_service.py +98 -43
- natural_pdf/core/page.py +86 -20
- natural_pdf/core/pdf.py +0 -2
- natural_pdf/describe/base.py +40 -9
- natural_pdf/describe/elements.py +11 -6
- natural_pdf/elements/base.py +134 -20
- natural_pdf/elements/collections.py +43 -11
- natural_pdf/elements/image.py +43 -0
- natural_pdf/elements/region.py +64 -19
- natural_pdf/elements/text.py +118 -11
- natural_pdf/flows/collections.py +4 -4
- natural_pdf/flows/region.py +17 -2
- natural_pdf/ocr/ocr_manager.py +50 -0
- natural_pdf/selectors/parser.py +27 -7
- natural_pdf/tables/__init__.py +5 -0
- natural_pdf/tables/result.py +101 -0
- natural_pdf/utils/bidi_mirror.py +36 -0
- natural_pdf/utils/visualization.py +15 -1
- {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/METADATA +2 -1
- {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/RECORD +48 -26
- natural_pdf-0.1.31.dist-info/top_level.txt +6 -0
- optimization/memory_comparison.py +172 -0
- optimization/pdf_analyzer.py +410 -0
- optimization/performance_analysis.py +397 -0
- optimization/test_cleanup_methods.py +155 -0
- optimization/test_memory_fix.py +162 -0
- tools/bad_pdf_eval/__init__.py +1 -0
- tools/bad_pdf_eval/analyser.py +302 -0
- tools/bad_pdf_eval/collate_summaries.py +130 -0
- tools/bad_pdf_eval/eval_suite.py +116 -0
- tools/bad_pdf_eval/export_enrichment_csv.py +62 -0
- tools/bad_pdf_eval/llm_enrich.py +273 -0
- tools/bad_pdf_eval/reporter.py +17 -0
- tools/bad_pdf_eval/utils.py +127 -0
- tools/rtl_smoke_test.py +80 -0
- natural_pdf-0.1.28.dist-info/top_level.txt +0 -2
- {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/WHEEL +0 -0
- {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/entry_points.txt +0 -0
- {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,397 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Natural PDF Performance Analysis Micro-Suite
|
4
|
+
|
5
|
+
This script analyzes memory usage and performance characteristics of Natural PDF
|
6
|
+
operations using real large PDFs to inform memory management decisions.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import gc
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
import psutil
|
13
|
+
import sys
|
14
|
+
import time
|
15
|
+
import tracemalloc
|
16
|
+
from dataclasses import dataclass, asdict
|
17
|
+
from pathlib import Path
|
18
|
+
from typing import Dict, List, Optional, Any, Callable
|
19
|
+
import pandas as pd
|
20
|
+
import matplotlib.pyplot as plt
|
21
|
+
|
22
|
+
import natural_pdf as npdf
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class MemorySnapshot:
|
27
|
+
"""Snapshot of memory usage at a point in time"""
|
28
|
+
timestamp: float
|
29
|
+
rss_mb: float # Resident Set Size
|
30
|
+
vms_mb: float # Virtual Memory Size
|
31
|
+
python_objects: int
|
32
|
+
operation: str
|
33
|
+
page_count: int
|
34
|
+
pdf_name: str
|
35
|
+
additional_info: Dict[str, Any]
|
36
|
+
|
37
|
+
|
38
|
+
class PerformanceProfiler:
|
39
|
+
"""Profiles memory usage and performance of Natural PDF operations"""
|
40
|
+
|
41
|
+
def __init__(self, output_dir: str = "performance_results"):
|
42
|
+
self.output_dir = Path(output_dir)
|
43
|
+
self.output_dir.mkdir(exist_ok=True)
|
44
|
+
|
45
|
+
self.snapshots: List[MemorySnapshot] = []
|
46
|
+
self.process = psutil.Process()
|
47
|
+
self.start_time = time.time()
|
48
|
+
|
49
|
+
# Start tracemalloc for detailed Python memory tracking
|
50
|
+
tracemalloc.start()
|
51
|
+
|
52
|
+
def take_snapshot(self, operation: str, page_count: int = 0,
|
53
|
+
pdf_name: str = "", **additional_info):
|
54
|
+
"""Take a memory usage snapshot"""
|
55
|
+
gc.collect() # Force garbage collection for accurate measurement
|
56
|
+
|
57
|
+
memory_info = self.process.memory_info()
|
58
|
+
python_objects = len(gc.get_objects())
|
59
|
+
|
60
|
+
snapshot = MemorySnapshot(
|
61
|
+
timestamp=time.time() - self.start_time,
|
62
|
+
rss_mb=memory_info.rss / 1024 / 1024,
|
63
|
+
vms_mb=memory_info.vms / 1024 / 1024,
|
64
|
+
python_objects=python_objects,
|
65
|
+
operation=operation,
|
66
|
+
page_count=page_count,
|
67
|
+
pdf_name=pdf_name,
|
68
|
+
additional_info=additional_info
|
69
|
+
)
|
70
|
+
|
71
|
+
self.snapshots.append(snapshot)
|
72
|
+
print(f"[{snapshot.timestamp:.1f}s] {operation}: {snapshot.rss_mb:.1f}MB RSS, {python_objects} objects")
|
73
|
+
|
74
|
+
def save_results(self, test_name: str):
|
75
|
+
"""Save results to JSON and CSV"""
|
76
|
+
# Convert to list of dicts for JSON serialization
|
77
|
+
data = [asdict(s) for s in self.snapshots]
|
78
|
+
|
79
|
+
# Save JSON
|
80
|
+
json_path = self.output_dir / f"{test_name}_snapshots.json"
|
81
|
+
with open(json_path, 'w') as f:
|
82
|
+
json.dump(data, f, indent=2)
|
83
|
+
|
84
|
+
# Save CSV for easy analysis
|
85
|
+
df = pd.DataFrame(data)
|
86
|
+
csv_path = self.output_dir / f"{test_name}_snapshots.csv"
|
87
|
+
df.to_csv(csv_path, index=False)
|
88
|
+
|
89
|
+
print(f"Results saved to {json_path} and {csv_path}")
|
90
|
+
return df
|
91
|
+
|
92
|
+
|
93
|
+
class PDFPerformanceTester:
|
94
|
+
"""Tests specific PDF operations and measures their performance"""
|
95
|
+
|
96
|
+
def __init__(self, pdf_path: str, profiler: PerformanceProfiler):
|
97
|
+
self.pdf_path = Path(pdf_path)
|
98
|
+
self.pdf_name = self.pdf_path.stem
|
99
|
+
self.profiler = profiler
|
100
|
+
self.pdf = None
|
101
|
+
|
102
|
+
def test_load_pdf(self):
|
103
|
+
"""Test just loading the PDF"""
|
104
|
+
self.profiler.take_snapshot("before_load", pdf_name=self.pdf_name)
|
105
|
+
|
106
|
+
self.pdf = npdf.PDF(str(self.pdf_path))
|
107
|
+
|
108
|
+
self.profiler.take_snapshot("after_load", pdf_name=self.pdf_name,
|
109
|
+
total_pages=len(self.pdf.pages))
|
110
|
+
|
111
|
+
def test_page_access(self, max_pages: int = 10):
|
112
|
+
"""Test accessing pages sequentially"""
|
113
|
+
if not self.pdf:
|
114
|
+
self.test_load_pdf()
|
115
|
+
|
116
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
117
|
+
|
118
|
+
for i in range(pages_to_test):
|
119
|
+
page = self.pdf.pages[i]
|
120
|
+
|
121
|
+
# Just access the page to trigger lazy loading
|
122
|
+
_ = page.width, page.height
|
123
|
+
|
124
|
+
self.profiler.take_snapshot(
|
125
|
+
f"page_access_{i+1}",
|
126
|
+
page_count=i+1,
|
127
|
+
pdf_name=self.pdf_name,
|
128
|
+
page_width=page.width,
|
129
|
+
page_height=page.height
|
130
|
+
)
|
131
|
+
|
132
|
+
def test_describe_pages(self, max_pages: int = 5):
|
133
|
+
"""Test using .describe() on pages"""
|
134
|
+
if not self.pdf:
|
135
|
+
self.test_load_pdf()
|
136
|
+
|
137
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
138
|
+
|
139
|
+
for i in range(pages_to_test):
|
140
|
+
page = self.pdf.pages[i]
|
141
|
+
|
142
|
+
# Use describe to understand page content
|
143
|
+
try:
|
144
|
+
description = page.describe()
|
145
|
+
|
146
|
+
self.profiler.take_snapshot(
|
147
|
+
f"describe_{i+1}",
|
148
|
+
page_count=i+1,
|
149
|
+
pdf_name=self.pdf_name,
|
150
|
+
description_length=len(description) if description else 0
|
151
|
+
)
|
152
|
+
except Exception as e:
|
153
|
+
self.profiler.take_snapshot(
|
154
|
+
f"describe_{i+1}_error",
|
155
|
+
page_count=i+1,
|
156
|
+
pdf_name=self.pdf_name,
|
157
|
+
error=str(e)
|
158
|
+
)
|
159
|
+
|
160
|
+
def test_element_collections(self, max_pages: int = 5):
|
161
|
+
"""Test find_all operations that create element collections"""
|
162
|
+
if not self.pdf:
|
163
|
+
self.test_load_pdf()
|
164
|
+
|
165
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
166
|
+
|
167
|
+
for i in range(pages_to_test):
|
168
|
+
page = self.pdf.pages[i]
|
169
|
+
|
170
|
+
# Test different element collection operations
|
171
|
+
operations = [
|
172
|
+
("words", lambda p: p.find_all("words")),
|
173
|
+
("text_elements", lambda p: p.find_all("text")),
|
174
|
+
("rects", lambda p: p.find_all("rect")),
|
175
|
+
("large_text", lambda p: p.find_all("text[size>12]")),
|
176
|
+
]
|
177
|
+
|
178
|
+
for op_name, operation in operations:
|
179
|
+
try:
|
180
|
+
elements = operation(page)
|
181
|
+
element_count = len(elements) if elements else 0
|
182
|
+
|
183
|
+
self.profiler.take_snapshot(
|
184
|
+
f"{op_name}_{i+1}",
|
185
|
+
page_count=i+1,
|
186
|
+
pdf_name=self.pdf_name,
|
187
|
+
operation_type=op_name,
|
188
|
+
element_count=element_count
|
189
|
+
)
|
190
|
+
except Exception as e:
|
191
|
+
self.profiler.take_snapshot(
|
192
|
+
f"{op_name}_{i+1}_error",
|
193
|
+
page_count=i+1,
|
194
|
+
pdf_name=self.pdf_name,
|
195
|
+
operation_type=op_name,
|
196
|
+
error=str(e)
|
197
|
+
)
|
198
|
+
|
199
|
+
def test_image_generation(self, max_pages: int = 3, resolutions: List[int] = [72, 144, 216]):
|
200
|
+
"""Test image generation at different resolutions"""
|
201
|
+
if not self.pdf:
|
202
|
+
self.test_load_pdf()
|
203
|
+
|
204
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
205
|
+
|
206
|
+
for i in range(pages_to_test):
|
207
|
+
page = self.pdf.pages[i]
|
208
|
+
|
209
|
+
for resolution in resolutions:
|
210
|
+
try:
|
211
|
+
img = page.to_image(resolution=resolution)
|
212
|
+
|
213
|
+
self.profiler.take_snapshot(
|
214
|
+
f"image_{resolution}dpi_{i+1}",
|
215
|
+
page_count=i+1,
|
216
|
+
pdf_name=self.pdf_name,
|
217
|
+
resolution=resolution,
|
218
|
+
image_size=f"{img.width}x{img.height}" if img else "None"
|
219
|
+
)
|
220
|
+
|
221
|
+
# Clean up image immediately to test memory release
|
222
|
+
del img
|
223
|
+
|
224
|
+
except Exception as e:
|
225
|
+
self.profiler.take_snapshot(
|
226
|
+
f"image_{resolution}dpi_{i+1}_error",
|
227
|
+
page_count=i+1,
|
228
|
+
pdf_name=self.pdf_name,
|
229
|
+
resolution=resolution,
|
230
|
+
error=str(e)
|
231
|
+
)
|
232
|
+
|
233
|
+
def test_ocr(self, max_pages: int = 2):
|
234
|
+
"""Test OCR operations (expensive!)"""
|
235
|
+
if not self.pdf:
|
236
|
+
self.test_load_pdf()
|
237
|
+
|
238
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
239
|
+
|
240
|
+
for i in range(pages_to_test):
|
241
|
+
page = self.pdf.pages[i]
|
242
|
+
|
243
|
+
try:
|
244
|
+
# Run OCR
|
245
|
+
page.apply_ocr(engine="easyocr") # Default engine
|
246
|
+
|
247
|
+
self.profiler.take_snapshot(
|
248
|
+
f"ocr_{i+1}",
|
249
|
+
page_count=i+1,
|
250
|
+
pdf_name=self.pdf_name,
|
251
|
+
operation_type="ocr"
|
252
|
+
)
|
253
|
+
|
254
|
+
except Exception as e:
|
255
|
+
self.profiler.take_snapshot(
|
256
|
+
f"ocr_{i+1}_error",
|
257
|
+
page_count=i+1,
|
258
|
+
pdf_name=self.pdf_name,
|
259
|
+
operation_type="ocr",
|
260
|
+
error=str(e)
|
261
|
+
)
|
262
|
+
|
263
|
+
def test_layout_analysis(self, max_pages: int = 3):
|
264
|
+
"""Test layout analysis operations"""
|
265
|
+
if not self.pdf:
|
266
|
+
self.test_load_pdf()
|
267
|
+
|
268
|
+
pages_to_test = min(max_pages, len(self.pdf.pages))
|
269
|
+
|
270
|
+
for i in range(pages_to_test):
|
271
|
+
page = self.pdf.pages[i]
|
272
|
+
|
273
|
+
try:
|
274
|
+
# Run layout analysis
|
275
|
+
layout_result = page.analyze_layout()
|
276
|
+
|
277
|
+
self.profiler.take_snapshot(
|
278
|
+
f"layout_{i+1}",
|
279
|
+
page_count=i+1,
|
280
|
+
pdf_name=self.pdf_name,
|
281
|
+
operation_type="layout",
|
282
|
+
layout_regions=len(layout_result) if layout_result else 0
|
283
|
+
)
|
284
|
+
|
285
|
+
except Exception as e:
|
286
|
+
self.profiler.take_snapshot(
|
287
|
+
f"layout_{i+1}_error",
|
288
|
+
page_count=i+1,
|
289
|
+
pdf_name=self.pdf_name,
|
290
|
+
operation_type="layout",
|
291
|
+
error=str(e)
|
292
|
+
)
|
293
|
+
|
294
|
+
|
295
|
+
def run_comprehensive_test(pdf_path: str, test_name: str):
|
296
|
+
"""Run a comprehensive test suite on a PDF"""
|
297
|
+
print(f"\n{'='*60}")
|
298
|
+
print(f"COMPREHENSIVE TEST: {test_name}")
|
299
|
+
print(f"PDF: {pdf_path}")
|
300
|
+
print(f"{'='*60}")
|
301
|
+
|
302
|
+
profiler = PerformanceProfiler()
|
303
|
+
tester = PDFPerformanceTester(pdf_path, profiler)
|
304
|
+
|
305
|
+
# Initial baseline
|
306
|
+
profiler.take_snapshot("baseline_start", pdf_name=Path(pdf_path).stem)
|
307
|
+
|
308
|
+
# Test sequence
|
309
|
+
print("\n1. Testing PDF Load...")
|
310
|
+
tester.test_load_pdf()
|
311
|
+
|
312
|
+
print("\n2. Testing Page Access...")
|
313
|
+
tester.test_page_access(max_pages=10)
|
314
|
+
|
315
|
+
print("\n3. Testing Describe Operations...")
|
316
|
+
tester.test_describe_pages(max_pages=5)
|
317
|
+
|
318
|
+
print("\n4. Testing Element Collections...")
|
319
|
+
tester.test_element_collections(max_pages=5)
|
320
|
+
|
321
|
+
print("\n5. Testing Image Generation...")
|
322
|
+
tester.test_image_generation(max_pages=3)
|
323
|
+
|
324
|
+
print("\n6. Testing Layout Analysis...")
|
325
|
+
tester.test_layout_analysis(max_pages=3)
|
326
|
+
|
327
|
+
# OCR test (only for image-heavy PDFs)
|
328
|
+
if "OCR" in pdf_path or "image" in test_name.lower():
|
329
|
+
print("\n7. Testing OCR (Image-heavy PDF)...")
|
330
|
+
tester.test_ocr(max_pages=2)
|
331
|
+
|
332
|
+
# Final snapshot
|
333
|
+
profiler.take_snapshot("test_complete", pdf_name=Path(pdf_path).stem)
|
334
|
+
|
335
|
+
# Save results
|
336
|
+
df = profiler.save_results(test_name)
|
337
|
+
|
338
|
+
# Quick analysis
|
339
|
+
print(f"\n{'-'*40}")
|
340
|
+
print("QUICK ANALYSIS:")
|
341
|
+
print(f"Peak Memory: {df['rss_mb'].max():.1f} MB")
|
342
|
+
print(f"Memory Growth: {df['rss_mb'].iloc[-1] - df['rss_mb'].iloc[0]:.1f} MB")
|
343
|
+
print(f"Peak Objects: {df['python_objects'].max():,}")
|
344
|
+
print(f"Total Time: {df['timestamp'].iloc[-1]:.1f} seconds")
|
345
|
+
|
346
|
+
return df
|
347
|
+
|
348
|
+
|
349
|
+
def main():
|
350
|
+
"""Main test runner"""
|
351
|
+
print("Natural PDF Performance Analysis Micro-Suite")
|
352
|
+
print("=" * 50)
|
353
|
+
|
354
|
+
# Find test PDFs
|
355
|
+
large_pdfs_dir = Path("pdfs/hidden/large")
|
356
|
+
if not large_pdfs_dir.exists():
|
357
|
+
print(f"Error: {large_pdfs_dir} not found")
|
358
|
+
print("Please ensure large test PDFs are available")
|
359
|
+
return
|
360
|
+
|
361
|
+
# Expected test PDFs
|
362
|
+
test_pdfs = {
|
363
|
+
"text_heavy": large_pdfs_dir / "appendix_fy2026.pdf",
|
364
|
+
"image_heavy": large_pdfs_dir / "OCR 0802030-56.2022.8.14.0060_Cópia integral_Fazenda Marrocos.pdf"
|
365
|
+
}
|
366
|
+
|
367
|
+
results = {}
|
368
|
+
|
369
|
+
for test_name, pdf_path in test_pdfs.items():
|
370
|
+
if pdf_path.exists():
|
371
|
+
try:
|
372
|
+
results[test_name] = run_comprehensive_test(str(pdf_path), test_name)
|
373
|
+
except Exception as e:
|
374
|
+
print(f"Error testing {test_name}: {e}")
|
375
|
+
traceback.print_exc()
|
376
|
+
else:
|
377
|
+
print(f"Warning: {pdf_path} not found, skipping {test_name} test")
|
378
|
+
|
379
|
+
# Generate comparison report
|
380
|
+
if results:
|
381
|
+
print(f"\n{'='*60}")
|
382
|
+
print("COMPARISON SUMMARY")
|
383
|
+
print(f"{'='*60}")
|
384
|
+
|
385
|
+
for test_name, df in results.items():
|
386
|
+
print(f"\n{test_name.upper()}:")
|
387
|
+
print(f" Peak Memory: {df['rss_mb'].max():.1f} MB")
|
388
|
+
print(f" Memory Growth: {df['rss_mb'].iloc[-1] - df['rss_mb'].iloc[0]:.1f} MB")
|
389
|
+
print(f" Peak Objects: {df['python_objects'].max():,}")
|
390
|
+
print(f" Duration: {df['timestamp'].iloc[-1]:.1f}s")
|
391
|
+
|
392
|
+
print(f"\nResults saved to performance_results/ directory")
|
393
|
+
print("Use the CSV files for detailed analysis")
|
394
|
+
|
395
|
+
|
396
|
+
if __name__ == "__main__":
|
397
|
+
main()
|
@@ -0,0 +1,155 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Test script to verify the new cleanup methods work correctly.
|
4
|
+
|
5
|
+
This test verifies that:
|
6
|
+
1. Cleanup methods exist and are callable
|
7
|
+
2. They handle edge cases gracefully (empty caches, missing engines)
|
8
|
+
3. They actually clean up loaded models/engines
|
9
|
+
"""
|
10
|
+
|
11
|
+
import gc
|
12
|
+
import os
|
13
|
+
import sys
|
14
|
+
from pathlib import Path
|
15
|
+
import pytest
|
16
|
+
|
17
|
+
import natural_pdf as npdf
|
18
|
+
from natural_pdf.ocr.ocr_manager import OCRManager
|
19
|
+
from natural_pdf.analyzers.layout.layout_manager import LayoutManager
|
20
|
+
from natural_pdf.classification.manager import ClassificationManager
|
21
|
+
|
22
|
+
|
23
|
+
class TestCleanupMethods:
|
24
|
+
"""Test suite for manager cleanup methods"""
|
25
|
+
|
26
|
+
def test_ocr_manager_cleanup_empty(self):
|
27
|
+
"""Test OCR manager cleanup when no engines are loaded"""
|
28
|
+
manager = OCRManager()
|
29
|
+
|
30
|
+
# Test cleanup when nothing is loaded
|
31
|
+
count = manager.cleanup_engine()
|
32
|
+
assert count == 0, "Should return 0 when no engines loaded"
|
33
|
+
|
34
|
+
# Test cleanup of specific non-existent engine
|
35
|
+
count = manager.cleanup_engine("nonexistent")
|
36
|
+
assert count == 0, "Should return 0 when engine doesn't exist"
|
37
|
+
|
38
|
+
def test_layout_manager_cleanup_empty(self):
|
39
|
+
"""Test Layout manager cleanup when no detectors are loaded"""
|
40
|
+
manager = LayoutManager()
|
41
|
+
|
42
|
+
# Test cleanup when nothing is loaded
|
43
|
+
count = manager.cleanup_detector()
|
44
|
+
assert count == 0, "Should return 0 when no detectors loaded"
|
45
|
+
|
46
|
+
# Test cleanup of specific non-existent detector
|
47
|
+
count = manager.cleanup_detector("nonexistent")
|
48
|
+
assert count == 0, "Should return 0 when detector doesn't exist"
|
49
|
+
|
50
|
+
def test_classification_manager_cleanup_empty(self):
|
51
|
+
"""Test Classification manager cleanup when no models are loaded"""
|
52
|
+
try:
|
53
|
+
manager = ClassificationManager()
|
54
|
+
|
55
|
+
# Test cleanup when nothing is loaded
|
56
|
+
count = manager.cleanup_models()
|
57
|
+
assert count == 0, "Should return 0 when no models loaded"
|
58
|
+
|
59
|
+
# Test cleanup of specific non-existent model
|
60
|
+
count = manager.cleanup_models("nonexistent/model")
|
61
|
+
assert count == 0, "Should return 0 when model doesn't exist"
|
62
|
+
|
63
|
+
except ImportError:
|
64
|
+
pytest.skip("Classification dependencies not available")
|
65
|
+
|
66
|
+
def test_ocr_manager_cleanup_with_engine(self):
|
67
|
+
"""Test OCR manager cleanup after loading an engine"""
|
68
|
+
manager = OCRManager()
|
69
|
+
|
70
|
+
# Check if any OCR engines are available
|
71
|
+
available_engines = manager.get_available_engines()
|
72
|
+
if not available_engines:
|
73
|
+
pytest.skip("No OCR engines available for testing")
|
74
|
+
|
75
|
+
engine_name = available_engines[0]
|
76
|
+
print(f"Testing with OCR engine: {engine_name}")
|
77
|
+
|
78
|
+
# Load an engine by accessing it
|
79
|
+
try:
|
80
|
+
engine_instance = manager._get_engine_instance(engine_name)
|
81
|
+
assert engine_name in manager._engine_instances, "Engine should be cached"
|
82
|
+
|
83
|
+
# Test cleanup of specific engine
|
84
|
+
count = manager.cleanup_engine(engine_name)
|
85
|
+
assert count == 1, f"Should return 1 after cleaning up {engine_name}"
|
86
|
+
assert engine_name not in manager._engine_instances, "Engine should be removed from cache"
|
87
|
+
|
88
|
+
except Exception as e:
|
89
|
+
pytest.skip(f"Could not load {engine_name} engine: {e}")
|
90
|
+
|
91
|
+
def test_layout_manager_cleanup_with_detector(self):
|
92
|
+
"""Test Layout manager cleanup after loading a detector"""
|
93
|
+
manager = LayoutManager()
|
94
|
+
|
95
|
+
# Check if any layout engines are available
|
96
|
+
available_engines = manager.get_available_engines()
|
97
|
+
if not available_engines:
|
98
|
+
pytest.skip("No layout engines available for testing")
|
99
|
+
|
100
|
+
engine_name = available_engines[0]
|
101
|
+
print(f"Testing with layout engine: {engine_name}")
|
102
|
+
|
103
|
+
# Load a detector by accessing it
|
104
|
+
try:
|
105
|
+
detector_instance = manager._get_engine_instance(engine_name)
|
106
|
+
assert engine_name in manager._detector_instances, "Detector should be cached"
|
107
|
+
|
108
|
+
# Test cleanup of specific detector
|
109
|
+
count = manager.cleanup_detector(engine_name)
|
110
|
+
assert count == 1, f"Should return 1 after cleaning up {engine_name}"
|
111
|
+
assert engine_name not in manager._detector_instances, "Detector should be removed from cache"
|
112
|
+
|
113
|
+
except Exception as e:
|
114
|
+
pytest.skip(f"Could not load {engine_name} detector: {e}")
|
115
|
+
|
116
|
+
def test_methods_exist(self):
|
117
|
+
"""Test that all cleanup methods exist and are callable"""
|
118
|
+
# Test OCRManager
|
119
|
+
manager = OCRManager()
|
120
|
+
assert hasattr(manager, 'cleanup_engine'), "OCRManager should have cleanup_engine method"
|
121
|
+
assert callable(manager.cleanup_engine), "cleanup_engine should be callable"
|
122
|
+
|
123
|
+
# Test LayoutManager
|
124
|
+
layout_manager = LayoutManager()
|
125
|
+
assert hasattr(layout_manager, 'cleanup_detector'), "LayoutManager should have cleanup_detector method"
|
126
|
+
assert callable(layout_manager.cleanup_detector), "cleanup_detector should be callable"
|
127
|
+
|
128
|
+
# Test ClassificationManager (if available)
|
129
|
+
try:
|
130
|
+
classification_manager = ClassificationManager()
|
131
|
+
assert hasattr(classification_manager, 'cleanup_models'), "ClassificationManager should have cleanup_models method"
|
132
|
+
assert callable(classification_manager.cleanup_models), "cleanup_models should be callable"
|
133
|
+
except ImportError:
|
134
|
+
print("Classification dependencies not available, skipping ClassificationManager test")
|
135
|
+
|
136
|
+
|
137
|
+
def main():
|
138
|
+
"""Run the cleanup method tests"""
|
139
|
+
print("Testing manager cleanup methods...")
|
140
|
+
|
141
|
+
# Run pytest on just this file
|
142
|
+
exit_code = pytest.main([__file__, "-v", "-s"])
|
143
|
+
|
144
|
+
if exit_code == 0:
|
145
|
+
print("\n✅ All cleanup method tests passed!")
|
146
|
+
print("The memory management methods are working correctly.")
|
147
|
+
else:
|
148
|
+
print("\n❌ Some tests failed!")
|
149
|
+
print("The cleanup methods need investigation.")
|
150
|
+
|
151
|
+
return exit_code
|
152
|
+
|
153
|
+
|
154
|
+
if __name__ == "__main__":
|
155
|
+
exit(main())
|