360 lines
14 KiB
Python
360 lines
14 KiB
Python
## test_main.py
|
|
|
|
```python
|
|
"""
|
|
Comprehensive test suite for pdf_opener/main.py.
|
|
|
|
Tests cover PDFOpener class methods, parse_args function,
|
|
edge cases, error handling, and the main entry point.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import unittest
|
|
from io import StringIO
|
|
from typing import Optional
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
# Adjust import path since tests run from /data
|
|
sys.path.insert(0, "/data")
|
|
|
|
from pdf_opener.main import PDFOpener, parse_args, main # noqa: E402
|
|
|
|
|
|
## Helper Factories
|
|
|
|
def make_mock_page(text: str = "Sample page text") -> MagicMock:
|
|
"""Create a mock PDF page with configurable extract_text return value."""
|
|
page = MagicMock()
|
|
page.extract_text.return_value = text
|
|
return page
|
|
|
|
|
|
def make_mock_metadata(
|
|
title: Optional[str] = "Test Title",
|
|
author: Optional[str] = "Test Author",
|
|
subject: Optional[str] = "Test Subject",
|
|
creator: Optional[str] = "Test Creator",
|
|
producer: Optional[str] = "Test Producer",
|
|
creation_date: Optional[str] = "2024-01-01",
|
|
) -> MagicMock:
|
|
"""Create a mock PDF metadata object."""
|
|
metadata = MagicMock()
|
|
metadata.title = title
|
|
metadata.author = author
|
|
metadata.subject = subject
|
|
metadata.creator = creator
|
|
metadata.producer = producer
|
|
metadata.creation_date = creation_date
|
|
return metadata
|
|
|
|
|
|
def make_mock_reader(
|
|
num_pages: int = 3,
|
|
page_texts: Optional[list] = None,
|
|
metadata: Optional[MagicMock] = None,
|
|
) -> MagicMock:
|
|
"""Create a mock PdfReader with configurable pages and metadata."""
|
|
reader = MagicMock()
|
|
|
|
if page_texts is None:
|
|
page_texts = [f"Page {i + 1} content" for i in range(num_pages)]
|
|
|
|
pages = [make_mock_page(text) for text in page_texts]
|
|
reader.pages = pages
|
|
reader.metadata = metadata if metadata is not None else make_mock_metadata()
|
|
return reader
|
|
|
|
|
|
## TestPDFOpenerInit
|
|
|
|
class TestPDFOpenerInit(unittest.TestCase):
|
|
"""Tests for PDFOpener.__init__ method."""
|
|
|
|
def test_default_initialization(self) -> None:
|
|
"""Test PDFOpener initializes with correct default values."""
|
|
opener = PDFOpener(file_path="test.pdf")
|
|
self.assertEqual(opener._file_path, "test.pdf")
|
|
self.assertEqual(opener._start_page, 0)
|
|
self.assertIsNone(opener._end_page)
|
|
|
|
def test_custom_start_page(self) -> None:
|
|
"""Test PDFOpener initializes with custom start_page."""
|
|
opener = PDFOpener(file_path="test.pdf", start_page=2)
|
|
self.assertEqual(opener._start_page, 2)
|
|
|
|
def test_custom_end_page(self) -> None:
|
|
"""Test PDFOpener initializes with custom end_page."""
|
|
opener = PDFOpener(file_path="test.pdf", end_page=5)
|
|
self.assertEqual(opener._end_page, 5)
|
|
|
|
def test_full_custom_initialization(self) -> None:
|
|
"""Test PDFOpener initializes with all custom values."""
|
|
opener = PDFOpener(file_path="/path/to/doc.pdf", start_page=1, end_page=4)
|
|
self.assertEqual(opener._file_path, "/path/to/doc.pdf")
|
|
self.assertEqual(opener._start_page, 1)
|
|
self.assertEqual(opener._end_page, 4)
|
|
|
|
def test_file_path_type_is_str(self) -> None:
|
|
"""Test that file_path is stored as string."""
|
|
opener = PDFOpener(file_path="sample.pdf")
|
|
self.assertIsInstance(opener._file_path, str)
|
|
|
|
def test_start_page_type_is_int(self) -> None:
|
|
"""Test that start_page is stored as int."""
|
|
opener = PDFOpener(file_path="sample.pdf", start_page=3)
|
|
self.assertIsInstance(opener._start_page, int)
|
|
|
|
def test_end_page_none_by_default(self) -> None:
|
|
"""Test that end_page defaults to None."""
|
|
opener = PDFOpener(file_path="sample.pdf")
|
|
self.assertIsNone(opener._end_page)
|
|
|
|
|
|
## TestPDFOpenerOpen
|
|
|
|
class TestPDFOpenerOpen(unittest.TestCase):
|
|
"""Tests for PDFOpener.open method."""
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_returns_reader_on_success(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() returns a PdfReader instance on success."""
|
|
mock_reader_instance = MagicMock()
|
|
mock_pdf_reader.return_value = mock_reader_instance
|
|
|
|
opener = PDFOpener(file_path="valid.pdf")
|
|
result = opener.open()
|
|
|
|
mock_pdf_reader.assert_called_once_with("valid.pdf")
|
|
self.assertEqual(result, mock_reader_instance)
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_file_not_found_exits(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() calls sys.exit(1) when file is not found."""
|
|
mock_pdf_reader.side_effect = FileNotFoundError
|
|
|
|
opener = PDFOpener(file_path="nonexistent.pdf")
|
|
with self.assertRaises(SystemExit) as context:
|
|
opener.open()
|
|
self.assertEqual(context.exception.code, 1)
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_file_not_found_prints_error(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() prints error message to stderr on FileNotFoundError."""
|
|
mock_pdf_reader.side_effect = FileNotFoundError
|
|
|
|
opener = PDFOpener(file_path="nonexistent.pdf")
|
|
with self.assertRaises(SystemExit):
|
|
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
|
|
opener.open()
|
|
self.assertIn("nonexistent.pdf", mock_stderr.getvalue())
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_pdf_read_error_exits(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() calls sys.exit(1) on PdfReadError."""
|
|
import pypdf
|
|
mock_pdf_reader.side_effect = pypdf.errors.PdfReadError("corrupted")
|
|
|
|
opener = PDFOpener(file_path="corrupted.pdf")
|
|
with self.assertRaises(SystemExit) as context:
|
|
opener.open()
|
|
self.assertEqual(context.exception.code, 1)
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_pdf_read_error_prints_error(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() prints error message to stderr on PdfReadError."""
|
|
import pypdf
|
|
mock_pdf_reader.side_effect = pypdf.errors.PdfReadError("corrupted")
|
|
|
|
opener = PDFOpener(file_path="corrupted.pdf")
|
|
with self.assertRaises(SystemExit):
|
|
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
|
|
opener.open()
|
|
self.assertIn("corrupted", mock_stderr.getvalue())
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_permission_error_exits(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() calls sys.exit(1) on PermissionError."""
|
|
mock_pdf_reader.side_effect = PermissionError
|
|
|
|
opener = PDFOpener(file_path="protected.pdf")
|
|
with self.assertRaises(SystemExit) as context:
|
|
opener.open()
|
|
self.assertEqual(context.exception.code, 1)
|
|
|
|
@patch("pdf_opener.main.PdfReader")
|
|
def test_open_permission_error_prints_error(self, mock_pdf_reader: MagicMock) -> None:
|
|
"""Test that open() prints error message to stderr on PermissionError."""
|
|
mock_pdf_reader.side_effect = PermissionError
|
|
|
|
opener = PDFOpener(file_path="protected.pdf")
|
|
with self.assertRaises(SystemExit):
|
|
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
|
|
opener.open()
|
|
self.assertIn("protected.pdf", mock_stderr.getvalue())
|
|
|
|
|
|
## TestPDFOpenerGetInfo
|
|
|
|
class TestPDFOpenerGetInfo(unittest.TestCase):
|
|
"""Tests for PDFOpener.get_info method."""
|
|
|
|
def setUp(self) -> None:
|
|
"""Set up common test fixtures."""
|
|
self.opener = PDFOpener(file_path="test.pdf")
|
|
|
|
def test_get_info_returns_dict(self) -> None:
|
|
"""Test that get_info returns a dictionary."""
|
|
reader = make_mock_reader(num_pages=5)
|
|
result = self.opener.get_info(reader)
|
|
self.assertIsInstance(result, dict)
|
|
|
|
def test_get_info_num_pages(self) -> None:
|
|
"""Test that get_info correctly reports number of pages."""
|
|
reader = make_mock_reader(num_pages=7)
|
|
result = self.opener.get_info(reader)
|
|
self.assertEqual(result["num_pages"], 7)
|
|
|
|
def test_get_info_with_full_metadata(self) -> None:
|
|
"""Test get_info with complete metadata."""
|
|
metadata = make_mock_metadata(
|
|
title="My PDF",
|
|
author="John Doe",
|
|
subject="Testing",
|
|
creator="Word",
|
|
producer="Adobe",
|
|
creation_date="2023-06-15",
|
|
)
|
|
reader = make_mock_reader(num_pages=3, metadata=metadata)
|
|
result = self.opener.get_info(reader)
|
|
|
|
self.assertEqual(result["title"], "My PDF")
|
|
self.assertEqual(result["author"], "John Doe")
|
|
self.assertEqual(result["subject"], "Testing")
|
|
self.assertEqual(result["creator"], "Word")
|
|
self.assertEqual(result["producer"], "Adobe")
|
|
self.assertEqual(result["creation_date"], "2023-06-15")
|
|
|
|
def test_get_info_with_none_metadata(self) -> None:
|
|
"""Test get_info when metadata is None."""
|
|
reader = make_mock_reader(num_pages=2, metadata=None)
|
|
result = self.opener.get_info(reader)
|
|
|
|
self.assertEqual(result["title"], "未知")
|
|
self.assertEqual(result["author"], "未知")
|
|
self.assertEqual(result["subject"], "未知")
|
|
self.assertEqual(result["creator"], "未知")
|
|
self.assertEqual(result["producer"], "未知")
|
|
self.assertEqual(result["creation_date"], "未知")
|
|
|
|
def test_get_info_with_partial_metadata_none_fields(self) -> None:
|
|
"""Test get_info when some metadata fields are None."""
|
|
metadata = make_mock_metadata(
|
|
title=None,
|
|
author="Known Author",
|
|
subject=None,
|
|
creator=None,
|
|
producer="Known Producer",
|
|
creation_date=None,
|
|
)
|
|
reader = make_mock_reader(num_pages=1, metadata=metadata)
|
|
result = self.opener.get_info(reader)
|
|
|
|
self.assertEqual(result["title"], "未知")
|
|
self.assertEqual(result["author"], "Known Author")
|
|
self.assertEqual(result["subject"], "未知")
|
|
self.assertEqual(result["creator"], "未知")
|
|
self.assertEqual(result["producer"], "Known Producer")
|
|
self.assertEqual(result["creation_date"], "未知")
|
|
|
|
def test_get_info_contains_all_keys(self) -> None:
|
|
"""Test that get_info result contains all expected keys."""
|
|
reader = make_mock_reader(num_pages=1)
|
|
result = self.opener.get_info(reader)
|
|
|
|
expected_keys = [
|
|
"num_pages", "title", "author", "subject",
|
|
"creator", "producer", "creation_date"
|
|
]
|
|
for key in expected_keys:
|
|
self.assertIn(key, result)
|
|
|
|
def test_get_info_single_page_pdf(self) -> None:
|
|
"""Test get_info with a single page PDF."""
|
|
reader = make_mock_reader(num_pages=1)
|
|
result = self.opener.get_info(reader)
|
|
self.assertEqual(result["num_pages"], 1)
|
|
|
|
|
|
## TestPDFOpenerDisplayInfo
|
|
|
|
class TestPDFOpenerDisplayInfo(unittest.TestCase):
|
|
"""Tests for PDFOpener.display_info method."""
|
|
|
|
def setUp(self) -> None:
|
|
"""Set up common test fixtures."""
|
|
self.opener = PDFOpener(file_path="test.pdf")
|
|
self.sample_info: dict = {
|
|
"num_pages": 10,
|
|
"title": "Sample Title",
|
|
"author": "Sample Author",
|
|
"subject": "Sample Subject",
|
|
"creator": "Sample Creator",
|
|
"producer": "Sample Producer",
|
|
"creation_date": "2024-01-01",
|
|
}
|
|
|
|
def test_display_info_prints_num_pages(self) -> None:
|
|
"""Test that display_info prints the number of pages."""
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(self.sample_info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("10", output)
|
|
|
|
def test_display_info_prints_title(self) -> None:
|
|
"""Test that display_info prints the title."""
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(self.sample_info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("Sample Title", output)
|
|
|
|
def test_display_info_prints_author(self) -> None:
|
|
"""Test that display_info prints the author."""
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(self.sample_info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("Sample Author", output)
|
|
|
|
def test_display_info_prints_separator(self) -> None:
|
|
"""Test that display_info prints separator lines."""
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(self.sample_info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("=" * 50, output)
|
|
|
|
def test_display_info_prints_header(self) -> None:
|
|
"""Test that display_info prints the header text."""
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(self.sample_info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("PDF 文件基本信息", output)
|
|
|
|
def test_display_info_with_unknown_values(self) -> None:
|
|
"""Test display_info with unknown/missing values."""
|
|
info: dict = {
|
|
"num_pages": 1,
|
|
"title": "未知",
|
|
"author": "未知",
|
|
"subject": "未知",
|
|
"creator": "未知",
|
|
"producer": "未知",
|
|
"creation_date": "未知",
|
|
}
|
|
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
|
self.opener.display_info(info)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("未知", output)
|
|
|
|
def test_display_info_with_empty_dict(self) -> None:
|
|
"""Test display_info with empty dict uses default '未知'."""
|
|
with patch("sys.stdout", new_callable=StringIO) as |