#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Test file for: compile_tex_structure.py import os import re import sys from pathlib import Path import pytest # Add scripts/python to path for imports ROOT_DIR = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(ROOT_DIR / "scripts" / "python")) from compile_tex_structure import ( # noqa: E402 compile_tex_structure, expand_inputs, generate_signature, ) class TestGenerateSignature: """Test signature generation.""" def test_generate_signature_contains_scitex_writer(self): """Signature should contain 'SciTeX Writer'.""" signature = generate_signature() assert "SciTeX Writer" in signature def test_generate_signature_contains_version(self): """Signature should contain version number or 'unknown'.""" signature = generate_signature() # Should have version line assert "SciTeX Writer" in signature # Version is present (either from pyproject.toml or 'unknown') assert "SciTeX Writer unknown" in signature or re.search( r"SciTeX Writer \d+\.\d+", signature ) def test_generate_signature_contains_engine(self): """Signature should contain engine information.""" signature = generate_signature() assert "LaTeX compilation engine:" in signature # Default should be 'auto' if env vars not set assert "auto" in signature or "tectonic" in signature or "latexmk" in signature def test_generate_signature_with_env_engine(self, monkeypatch): """Signature should use SCITEX_WRITER_ENGINE if set.""" monkeypatch.setenv("SCITEX_WRITER_ENGINE", "tectonic") signature = generate_signature() assert "engine: tectonic" in signature def test_generate_signature_with_source_file(self, tmp_path): """Signature should include source file path when provided.""" source_file = tmp_path / "test.tex" signature = generate_signature(source_file=source_file) assert "Source:" in signature assert str(source_file) in signature def test_generate_signature_has_timestamp(self): """Signature should contain compilation timestamp.""" signature = generate_signature() assert "Compiled:" in signature # Should match date format YYYY-MM-DD assert re.search(r"\d{4}-\d{2}-\d{2}", signature) def test_generate_signature_is_comment(self): """Signature should be LaTeX comments.""" signature = generate_signature() lines = signature.split("\n") # Non-empty lines should start with % for line in lines: if line.strip(): assert line.startswith("%") class TestExpandInputs: """Test TeX input expansion.""" def test_expand_inputs_simple_file(self, tmp_path): """Should read and return simple file content.""" test_file = tmp_path / "test.tex" test_file.write_text("Hello, world!") result = expand_inputs(test_file) assert "Hello, world!" in result def test_expand_inputs_with_input_command(self, tmp_path): """Should expand \\input{} commands.""" # Create child file child_file = tmp_path / "child.tex" child_file.write_text("Child content here") # Create parent file that references child parent_file = tmp_path / "parent.tex" parent_file.write_text("Start\n\\input{child}\nEnd") result = expand_inputs(parent_file) # Should contain both parent and child content assert "Start" in result assert "Child content here" in result assert "End" in result # Should have header comment for expanded file assert "File: child" in result def test_expand_inputs_missing_file(self, tmp_path): """Should handle missing input file gracefully.""" parent_file = tmp_path / "parent.tex" parent_file.write_text("\\input{nonexistent}") result = expand_inputs(parent_file) # Should contain SKIPPED comment assert "SKIPPED" in result assert "file not found" in result def test_expand_inputs_circular_reference(self, tmp_path): """Should detect and prevent circular references.""" # Create file that references itself self_ref_file = tmp_path / "circular.tex" self_ref_file.write_text("Start\n\\input{circular}\nEnd") result = expand_inputs(self_ref_file) # Should contain circular reference warning assert "SKIPPED" in result assert "circular reference" in result or "already processed" in result def test_expand_inputs_max_depth(self, tmp_path): """Should stop at max recursion depth.""" # Create deeply nested structure files = [] for i in range(15): f = tmp_path / f"level_{i}.tex" if i < 14: f.write_text(f"Level {i}\n\\input{{level_{i + 1}}}") else: f.write_text(f"Level {i}") files.append(f) result = expand_inputs(files[0], max_depth=5) # Should stop before reaching the deepest level assert "Level 0" in result assert "Level 5" in result or "Level 6" in result # Should hit depth limit assert "ERROR" in result or "Max recursion depth" in result def test_expand_inputs_commented_line_skipped(self, tmp_path): """Should skip \\input{} in commented lines.""" child_file = tmp_path / "child.tex" child_file.write_text("This should not appear") parent_file = tmp_path / "parent.tex" parent_file.write_text("Start\n% \\input{child}\nEnd") result = expand_inputs(parent_file) # Should NOT expand commented input assert "This should not appear" not in result # Original comment should be preserved assert "% \\input{child}" in result def test_expand_inputs_adds_tex_extension(self, tmp_path): """Should add .tex extension if not present.""" child_file = tmp_path / "child.tex" child_file.write_text("Child content") parent_file = tmp_path / "parent.tex" # Reference without .tex extension parent_file.write_text("\\input{child}") result = expand_inputs(parent_file) # Should successfully expand even without .tex assert "Child content" in result def test_expand_inputs_nested_multiple_levels(self, tmp_path): """Should handle multiple levels of nesting.""" level2 = tmp_path / "level2.tex" level2.write_text("Level 2 content") level1 = tmp_path / "level1.tex" level1.write_text("Level 1 start\n\\input{level2}\nLevel 1 end") level0 = tmp_path / "level0.tex" level0.write_text("Level 0 start\n\\input{level1}\nLevel 0 end") result = expand_inputs(level0) # All levels should be present assert "Level 0 start" in result assert "Level 1 start" in result assert "Level 2 content" in result assert "Level 1 end" in result assert "Level 0 end" in result def test_expand_inputs_nonexistent_file(self, tmp_path): """Should handle nonexistent file at top level.""" nonexistent = tmp_path / "does_not_exist.tex" result = expand_inputs(nonexistent) # Should return SKIPPED message assert "SKIPPED" in result assert "file not found" in result class TestCompileTexStructure: """Test full TeX structure compilation.""" def test_compile_tex_structure_creates_output(self, tmp_path): """Should create output file.""" base_file = tmp_path / "base.tex" base_file.write_text( "\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}" ) output_file = tmp_path / "output.tex" success = compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False ) assert success is True assert output_file.exists() def test_compile_tex_structure_contains_signature(self, tmp_path): """Output should contain compilation signature.""" base_file = tmp_path / "base.tex" base_file.write_text("Content") output_file = tmp_path / "output.tex" compile_tex_structure(base_tex=base_file, output_tex=output_file, verbose=False) output_content = output_file.read_text() assert "SciTeX Writer" in output_content assert "Compiled:" in output_content def test_compile_tex_structure_expands_inputs(self, tmp_path): """Should expand \\input{} commands in base file.""" child_file = tmp_path / "child.tex" child_file.write_text("Child content") base_file = tmp_path / "base.tex" base_file.write_text("Start\n\\input{child}\nEnd") output_file = tmp_path / "output.tex" compile_tex_structure(base_tex=base_file, output_tex=output_file, verbose=False) output_content = output_file.read_text() assert "Child content" in output_content def test_compile_tex_structure_missing_base_file(self, tmp_path): """Should return False for missing base file.""" base_file = tmp_path / "nonexistent.tex" output_file = tmp_path / "output.tex" success = compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False ) assert success is False assert not output_file.exists() def test_compile_tex_structure_creates_output_dir(self, tmp_path): """Should create output directory if it doesn't exist.""" base_file = tmp_path / "base.tex" base_file.write_text("Content") output_dir = tmp_path / "nested" / "dir" output_file = output_dir / "output.tex" success = compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False ) assert success is True assert output_dir.exists() assert output_file.exists() def test_tectonic_mode_comments_out_lineno(self, tmp_path): """Tectonic mode should comment out lineno package.""" base_file = tmp_path / "base.tex" base_file.write_text( "\\usepackage{lineno}\n\\linenumbers\n\\begin{document}\nContent\n\\end{document}" ) output_file = tmp_path / "output.tex" compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False, tectonic_mode=True, ) output_content = output_file.read_text() # lineno package should be commented out assert "% \\usepackage{lineno}" in output_content # linenumbers command should be commented out assert "% \\linenumbers" in output_content def test_tectonic_mode_comments_out_bashful(self, tmp_path): """Tectonic mode should comment out bashful package.""" base_file = tmp_path / "base.tex" base_file.write_text( "\\usepackage{bashful}\n\\begin{document}\nContent\n\\end{document}" ) output_file = tmp_path / "output.tex" compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False, tectonic_mode=True, ) output_content = output_file.read_text() # bashful package should be commented out assert "% \\usepackage{bashful}" in output_content def test_dark_mode_injection(self, tmp_path): """Dark mode should inject styling before document.""" # Create the dark_mode.tex file structure dark_mode_dir = tmp_path / "00_shared" / "latex_styles" dark_mode_dir.mkdir(parents=True) dark_mode_file = dark_mode_dir / "dark_mode.tex" dark_mode_file.write_text( "% Dark mode test content\n\\pagecolor{black}\n\\color{white}" ) base_file = tmp_path / "01_manuscript" / "base.tex" base_file.parent.mkdir(parents=True) base_file.write_text("\\begin{document}\nContent\n\\end{document}") output_file = tmp_path / "01_manuscript" / "output.tex" compile_tex_structure( base_tex=base_file, output_tex=output_file, verbose=False, dark_mode=True ) output_content = output_file.read_text() # Should have dark mode comment assert "Dark mode styling" in output_content # Should have dark mode content assert "pagecolor{black}" in output_content # Dark mode should come before \begin{document} dark_pos = output_content.find("Dark mode") doc_pos = output_content.find("\\begin{document}") assert dark_pos < doc_pos if __name__ == "__main__": pytest.main([os.path.abspath(__file__), "-v"])