#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# File: tests/python/test_writer_sections.py
"""Tests for Writer section API methods: get_section, read_section, write_section, _list_sections."""
from pathlib import Path
from unittest.mock import patch
import pytest
from scitex_writer._dataclasses.core._DocumentSection import DocumentSection
from scitex_writer.writer import Writer
# Path to the actual template used as the project dir for integration-style tests
TEMPLATE_DIR = Path(__file__).resolve().parent.parent.parent
@pytest.fixture
def writer():
"""Provide a Writer instance attached to the actual template project."""
with patch("scitex_writer.writer._find_git_root", return_value=TEMPLATE_DIR):
return Writer(TEMPLATE_DIR)
# ---------------------------------------------------------------------------
# get_section
# ---------------------------------------------------------------------------
class TestGetSection:
"""Tests for Writer.get_section."""
def test_get_section_returns_document_section_for_manuscript(self, writer):
"""Verify get_section returns a DocumentSection for a manuscript section."""
section = writer.get_section("abstract", "manuscript")
assert isinstance(section, DocumentSection)
def test_get_section_returns_document_section_for_shared(self, writer):
"""Verify get_section returns a DocumentSection for a shared section."""
section = writer.get_section("title", "shared")
assert isinstance(section, DocumentSection)
def test_get_section_manuscript_section_has_path(self, writer):
"""Verify returned DocumentSection has a .path attribute."""
section = writer.get_section("introduction", "manuscript")
assert hasattr(section, "path")
assert isinstance(section.path, Path)
def test_get_section_shared_section_has_path(self, writer):
"""Verify shared DocumentSection has a .path attribute."""
section = writer.get_section("authors", "shared")
assert hasattr(section, "path")
assert isinstance(section.path, Path)
def test_get_section_raises_value_error_for_invalid_doc_type(self, writer):
"""Verify get_section raises ValueError for an unknown doc_type."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("abstract", "nonexistent_type")
assert "nonexistent_type" in str(exc_info.value)
def test_get_section_error_message_lists_valid_doc_types(self, writer):
"""Verify ValueError for invalid doc_type includes valid options."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("abstract", "invalid")
error_msg = str(exc_info.value)
for valid_type in ("shared", "manuscript", "supplementary", "revision"):
assert valid_type in error_msg
def test_get_section_raises_value_error_for_invalid_section_name_in_manuscript(
self, writer
):
"""Verify get_section raises ValueError for a nonexistent section in manuscript."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("nonexistent_section_xyz", "manuscript")
assert "nonexistent_section_xyz" in str(exc_info.value)
def test_get_section_error_message_lists_available_sections_for_manuscript(
self, writer
):
"""Verify ValueError for invalid section name includes available section names."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("bogus_section", "manuscript")
error_msg = str(exc_info.value)
# The error message should mention at least one known valid section
assert "abstract" in error_msg or "introduction" in error_msg
def test_get_section_raises_value_error_for_invalid_section_name_in_shared(
self, writer
):
"""Verify get_section raises ValueError for a nonexistent section in shared."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("nonexistent_shared_section", "shared")
assert "nonexistent_shared_section" in str(exc_info.value)
def test_get_section_error_message_lists_available_sections_for_shared(
self, writer
):
"""Verify ValueError for invalid shared section name includes available sections."""
with pytest.raises(ValueError) as exc_info:
writer.get_section("bogus_shared_key", "shared")
error_msg = str(exc_info.value)
assert "title" in error_msg or "authors" in error_msg
def test_get_section_manuscript_routes_via_contents(self, writer):
"""Verify manuscript sections are accessed via .contents attribute."""
section = writer.get_section("abstract", "manuscript")
# Path should be inside the manuscript contents directory
assert "01_manuscript" in str(section.path)
assert "contents" in str(section.path)
def test_get_section_shared_routes_directly(self, writer):
"""Verify shared sections are accessed directly (not via .contents)."""
section = writer.get_section("title", "shared")
# Path should be inside the shared directory
assert "00_shared" in str(section.path)
def test_get_section_supplementary_returns_document_section(self, writer):
"""Verify get_section works for supplementary doc_type."""
section = writer.get_section("methods", "supplementary")
assert isinstance(section, DocumentSection)
def test_get_section_revision_returns_document_section(self, writer):
"""Verify get_section works for revision doc_type."""
section = writer.get_section("introduction", "revision")
assert isinstance(section, DocumentSection)
def test_get_section_section_has_read_method(self, writer):
"""Verify returned DocumentSection has a .read() method."""
section = writer.get_section("abstract", "manuscript")
assert callable(getattr(section, "read", None))
def test_get_section_section_has_write_method(self, writer):
"""Verify returned DocumentSection has a .write() method."""
section = writer.get_section("abstract", "manuscript")
assert callable(getattr(section, "write", None))
# ---------------------------------------------------------------------------
# read_section
# ---------------------------------------------------------------------------
class TestReadSection:
"""Tests for Writer.read_section."""
def test_read_section_returns_string(self, writer):
"""Verify read_section returns a string."""
content = writer.read_section("abstract", "manuscript")
assert isinstance(content, str)
def test_read_section_shared_title_returns_string(self, writer):
"""Verify read_section returns a string for shared title."""
content = writer.read_section("title", "shared")
assert isinstance(content, str)
def test_read_section_returns_empty_string_for_missing_file(self, writer):
"""Verify read_section returns empty string when section file does not exist."""
# Use a section that definitely doesn't have a real file in a tmp setup
# We mock a DocumentSection whose .read() returns None
with patch.object(writer, "get_section") as mock_get:
mock_section = DocumentSection(Path("/nonexistent/path.tex"))
mock_get.return_value = mock_section
content = writer.read_section("abstract", "manuscript")
assert content == ""
def test_read_section_manuscript_introduction(self, writer):
"""Verify read_section returns a string for manuscript introduction."""
content = writer.read_section("introduction", "manuscript")
assert isinstance(content, str)
def test_read_section_joins_list_content(self, writer):
"""Verify read_section joins list content into a single string."""
with patch.object(writer, "get_section") as mock_get:
mock_section = DocumentSection.__new__(DocumentSection)
mock_section.path = Path("/fake/section.tex")
mock_section.read = lambda: ["line one", "line two"]
mock_get.return_value = mock_section
content = writer.read_section("abstract", "manuscript")
assert content == "line one\nline two"
def test_read_section_raises_for_invalid_doc_type(self, writer):
"""Verify read_section propagates ValueError for invalid doc_type."""
with pytest.raises(ValueError):
writer.read_section("abstract", "bad_doc_type")
def test_read_section_raises_for_invalid_section_name(self, writer):
"""Verify read_section propagates ValueError for invalid section name."""
with pytest.raises(ValueError):
writer.read_section("totally_missing_section", "manuscript")
def test_read_section_default_doc_type_is_manuscript(self, writer):
"""Verify default doc_type is manuscript when omitted."""
content_explicit = writer.read_section("abstract", "manuscript")
content_default = writer.read_section("abstract")
assert content_explicit == content_default
# ---------------------------------------------------------------------------
# write_section
# ---------------------------------------------------------------------------
class TestWriteSection:
"""Tests for Writer.write_section."""
def test_write_section_returns_true_on_success(self, tmp_path):
"""Verify write_section returns True when write succeeds."""
_setup_minimal_project(tmp_path)
with patch("scitex_writer.writer._find_git_root", return_value=None):
w = Writer(tmp_path)
result = w.write_section("abstract", "Test abstract content.", "manuscript")
assert result is True
def test_write_section_content_is_readable_back(self, tmp_path):
"""Verify write_section writes content that read_section can read back."""
_setup_minimal_project(tmp_path)
with patch("scitex_writer.writer._find_git_root", return_value=None):
w = Writer(tmp_path)
test_content = "This is a unique test abstract for round-trip verification."
w.write_section("abstract", test_content, "manuscript")
result = w.read_section("abstract", "manuscript")
assert result == test_content
def test_write_section_overwrites_existing_content(self, tmp_path):
"""Verify write_section overwrites previously written content."""
_setup_minimal_project(tmp_path)
with patch("scitex_writer.writer._find_git_root", return_value=None):
w = Writer(tmp_path)
w.write_section("abstract", "First content.", "manuscript")
w.write_section("abstract", "Second content.", "manuscript")
result = w.read_section("abstract", "manuscript")
assert result == "Second content."
def test_write_section_shared_title(self, tmp_path):
"""Verify write_section works for shared doc_type."""
_setup_minimal_project(tmp_path)
with patch("scitex_writer.writer._find_git_root", return_value=None):
w = Writer(tmp_path)
test_title = "My Test Paper Title"
result = w.write_section("title", test_title, "shared")
assert result is True
assert w.read_section("title", "shared") == test_title
def test_write_section_default_doc_type_is_manuscript(self, tmp_path):
"""Verify default doc_type is manuscript when omitted."""
_setup_minimal_project(tmp_path)
with patch("scitex_writer.writer._find_git_root", return_value=None):
w = Writer(tmp_path)
test_content = "Abstract written with default doc_type."
w.write_section("abstract", test_content)
result = w.read_section("abstract", "manuscript")
assert result == test_content
def test_write_and_restore_actual_template(self, writer):
"""Verify write_section on actual template restores original content."""
original_content = writer.read_section("abstract", "manuscript")
try:
writer.write_section(
"abstract",
"Temporary test content for write_section round-trip.",
"manuscript",
)
modified_content = writer.read_section("abstract", "manuscript")
assert (
modified_content
== "Temporary test content for write_section round-trip."
)
finally:
# Always restore original content
writer.write_section("abstract", original_content, "manuscript")
restored_content = writer.read_section("abstract", "manuscript")
assert restored_content == original_content
def test_write_section_raises_for_invalid_doc_type(self, writer):
"""Verify write_section propagates ValueError for invalid doc_type."""
with pytest.raises(ValueError):
writer.write_section("abstract", "content", "bad_type")
def test_write_section_raises_for_invalid_section_name(self, writer):
"""Verify write_section propagates ValueError for invalid section name."""
with pytest.raises(ValueError):
writer.write_section("nonexistent_section", "content", "manuscript")
# ---------------------------------------------------------------------------
# _list_sections
# ---------------------------------------------------------------------------
class TestListSections:
"""Tests for Writer._list_sections."""
def test_list_sections_returns_list(self, writer):
"""Verify _list_sections returns a list."""
result = writer._list_sections(writer.manuscript.contents)
assert isinstance(result, list)
def test_list_sections_returns_list_of_strings(self, writer):
"""Verify _list_sections returns a list of strings."""
result = writer._list_sections(writer.manuscript.contents)
assert all(isinstance(name, str) for name in result)
def test_list_sections_manuscript_includes_abstract(self, writer):
"""Verify _list_sections for manuscript contents includes 'abstract'."""
result = writer._list_sections(writer.manuscript.contents)
assert "abstract" in result
def test_list_sections_manuscript_includes_introduction(self, writer):
"""Verify _list_sections for manuscript contents includes 'introduction'."""
result = writer._list_sections(writer.manuscript.contents)
assert "introduction" in result
def test_list_sections_manuscript_includes_methods(self, writer):
"""Verify _list_sections for manuscript contents includes 'methods'."""
result = writer._list_sections(writer.manuscript.contents)
assert "methods" in result
def test_list_sections_manuscript_includes_results(self, writer):
"""Verify _list_sections for manuscript contents includes 'results'."""
result = writer._list_sections(writer.manuscript.contents)
assert "results" in result
def test_list_sections_manuscript_includes_discussion(self, writer):
"""Verify _list_sections for manuscript contents includes 'discussion'."""
result = writer._list_sections(writer.manuscript.contents)
assert "discussion" in result
def test_list_sections_shared_includes_title(self, writer):
"""Verify _list_sections for shared tree includes 'title'."""
result = writer._list_sections(writer.shared)
assert "title" in result
def test_list_sections_shared_includes_authors(self, writer):
"""Verify _list_sections for shared tree includes 'authors'."""
result = writer._list_sections(writer.shared)
assert "authors" in result
def test_list_sections_shared_includes_keywords(self, writer):
"""Verify _list_sections for shared tree includes 'keywords'."""
result = writer._list_sections(writer.shared)
assert "keywords" in result
def test_list_sections_excludes_private_attributes(self, writer):
"""Verify _list_sections does not include names starting with underscore."""
result = writer._list_sections(writer.manuscript.contents)
for name in result:
assert not name.startswith("_")
def test_list_sections_excludes_path_only_attributes(self, writer):
"""Verify _list_sections excludes plain Path attributes (e.g., figures dir)."""
result = writer._list_sections(writer.manuscript.contents)
# 'figures' and 'tables' are plain Paths, not DocumentSections
assert "figures" not in result
assert "tables" not in result
def test_list_sections_non_empty_for_manuscript(self, writer):
"""Verify _list_sections returns non-empty list for manuscript contents."""
result = writer._list_sections(writer.manuscript.contents)
assert len(result) > 0
def test_list_sections_non_empty_for_shared(self, writer):
"""Verify _list_sections returns non-empty list for shared tree."""
result = writer._list_sections(writer.shared)
assert len(result) > 0
# ---------------------------------------------------------------------------
# shared vs manuscript routing
# ---------------------------------------------------------------------------
class TestSharedVsManuscriptRouting:
"""Tests for shared vs manuscript routing difference in get_section."""
def test_shared_title_path_is_in_shared_dir(self, writer):
"""Verify shared title section path is inside 00_shared."""
section = writer.get_section("title", "shared")
assert "00_shared" in str(section.path)
def test_manuscript_title_path_is_in_manuscript_contents(self, writer):
"""Verify manuscript title section path is inside 01_manuscript/contents."""
section = writer.get_section("title", "manuscript")
assert "01_manuscript" in str(section.path)
assert "contents" in str(section.path)
def test_shared_and_manuscript_title_are_different_paths(self, writer):
"""Verify shared and manuscript title sections point to different files."""
shared_section = writer.get_section("title", "shared")
manuscript_section = writer.get_section("title", "manuscript")
assert shared_section.path != manuscript_section.path
def test_get_section_manuscript_not_shared(self, writer):
"""Verify manuscript does not have extra shared-only attributes exposed directly."""
# 'bibliography' exists in manuscript contents but 'bib_files' (a plain Path) does not
manuscript_sections = writer._list_sections(writer.manuscript.contents)
shared_sections = writer._list_sections(writer.shared)
# Both trees expose 'bibliography' as a DocumentSection
assert "bibliography" in manuscript_sections
assert "bibliography" in shared_sections
def test_shared_doc_raises_for_manuscript_only_section(self, writer):
"""Verify 'introduction' is valid in manuscript but raises in shared."""
# Introduction exists in manuscript contents
section = writer.get_section("introduction", "manuscript")
assert isinstance(section, DocumentSection)
# Introduction does not exist in shared
with pytest.raises(ValueError):
writer.get_section("introduction", "shared")
# ---------------------------------------------------------------------------
# Helper utilities
# ---------------------------------------------------------------------------
def _setup_minimal_project(project_dir: Path) -> None:
"""Create a minimal valid project structure with required section files."""
(project_dir / "00_shared").mkdir(parents=True, exist_ok=True)
(project_dir / "01_manuscript" / "contents").mkdir(parents=True, exist_ok=True)
(project_dir / "02_supplementary" / "contents").mkdir(parents=True, exist_ok=True)
(project_dir / "03_revision" / "contents").mkdir(parents=True, exist_ok=True)
(project_dir / "scripts").mkdir(parents=True, exist_ok=True)
# Shared section files
shared_dir = project_dir / "00_shared"
(shared_dir / "title.tex").write_text("Test Title")
(shared_dir / "authors.tex").write_text("Test Author")
(shared_dir / "keywords.tex").write_text("test, keywords")
(shared_dir / "journal_name.tex").write_text("Test Journal")
bib_dir = shared_dir / "bib_files"
bib_dir.mkdir(parents=True, exist_ok=True)
(bib_dir / "bibliography.bib").write_text("")
# Manuscript section files
ms_contents = project_dir / "01_manuscript" / "contents"
for section in (
"abstract",
"introduction",
"methods",
"results",
"discussion",
"title",
"authors",
"keywords",
"journal_name",
"graphical_abstract",
"highlights",
"data_availability",
"additional_info",
"wordcount",
):
(ms_contents / f"{section}.tex").write_text(f"Placeholder {section}")
(ms_contents / "bibliography.bib").write_text("")
# Supplementary section files
sup_contents = project_dir / "02_supplementary" / "contents"
for section in (
"abstract",
"introduction",
"methods",
"results",
"discussion",
"title",
"authors",
"keywords",
"journal_name",
"graphical_abstract",
"highlights",
"data_availability",
"additional_info",
"wordcount",
):
(sup_contents / f"{section}.tex").write_text(f"Placeholder {section}")
(sup_contents / "bibliography.bib").write_text("")
# Revision section files
rev_contents = project_dir / "03_revision" / "contents"
for section in (
"abstract",
"introduction",
"methods",
"results",
"discussion",
"title",
"authors",
"keywords",
"journal_name",
"graphical_abstract",
"highlights",
"data_availability",
"additional_info",
"wordcount",
):
(rev_contents / f"{section}.tex").write_text(f"Placeholder {section}")
(rev_contents / "bibliography.bib").write_text("")
if __name__ == "__main__":
import os
import pytest
pytest.main([os.path.abspath(__file__)])