import contextlib import io import subprocess import shutil import tempfile import unittest from pathlib import Path from unittest.mock import patch from copienator import utils class CompileToPdfTests(unittest.TestCase): def test_source_preamble_keeps_macros_and_uses_fragment_layout(self): with tempfile.TemporaryDirectory() as directory: source = Path(directory) / 'source.tex' source.write_text('% \\begin{document}\n\\documentclass{article}\n' '\\usepackage{amsthm}\n\\newcommand{\\custom}{works}\n' '\\begin{document}original body\\end{document}') header = utils.fragment_latex_header(source) self.assertIn('standalone', header) self.assertIn(r'\usepackage{amsthm}', header) self.assertIn(r'\newcommand{\custom}{works}', header) self.assertNotIn('original body', header) @unittest.skipUnless(shutil.which('pdflatex'), 'pdflatex is not installed') def test_source_geometry_does_not_clip_fragment(self): import pymupdf with tempfile.TemporaryDirectory() as directory: source = Path(directory) / 'source.tex' source.write_text(r'\documentclass{article}' r'\usepackage[margin=1cm]{geometry}' r'\geometry{top=0.5cm}' r'\newcommand{\custom}{FIRSTLINE}' r'\begin{document}unused body\end{document}') output = Path(directory) / 'out.pdf' utils.compile_to_pdf(r'\custom\par LASTLINE', output, source_path=source, strict=True) with pymupdf.open(output) as doc: self.assertIn('FIRSTLINE', doc[0].get_text()) self.assertIn('LASTLINE', doc[0].get_text()) for word in doc[0].get_text('words'): self.assertGreaterEqual(word[1], 0) self.assertLessEqual(word[3], doc[0].rect.height) def test_source_directory_is_available_to_tex(self): with tempfile.TemporaryDirectory() as directory: source = Path(directory) / 'source.tex' source.write_text(r'\documentclass{article}\begin{document}') result = subprocess.CompletedProcess(args=[], returncode=1, stdout='! error') with patch.object(utils.subprocess, 'run', return_value=result) as run: with self.assertRaises(RuntimeError): utils.compile_to_pdf('broken', Path(directory) / 'out.pdf', source_path=source, strict=True) self.assertTrue(run.call_args.kwargs['env']['TEXINPUTS'].startswith(directory)) def test_strict_failure_preserves_previous_pdf(self): with tempfile.TemporaryDirectory() as directory: output = Path(directory) / 'out.pdf' output.write_bytes(b'previous PDF') def failed_run(*args, **kwargs): (Path(kwargs['cwd']) / 'text.pdf').write_bytes(b'partial') return subprocess.CompletedProcess(args=[], returncode=1, stdout='! error') with patch.object(utils.subprocess, 'run', side_effect=failed_run): with self.assertRaises(RuntimeError): utils.compile_to_pdf('broken', output, strict=True) self.assertEqual(output.read_bytes(), b'previous PDF') def test_strict_compilation_propagates_failure_with_context(self): result = subprocess.CompletedProcess(args=[], returncode=1, stdout="! Undefined control sequence.\nl.13 broken command\n") with tempfile.TemporaryDirectory() as directory, \ patch.object(utils.subprocess, "run", return_value=result) as run: with self.assertRaisesRegex(RuntimeError, "LaTeX failed") as error: utils.compile_to_pdf("broken", Path(directory) / "out.pdf", strict=True) self.assertIn("l.13 broken command", str(error.exception)) self.assertEqual(run.call_args.kwargs["errors"], "replace") def test_warns_when_latex_fails_even_if_partial_pdf_exists(self): with tempfile.TemporaryDirectory() as temporary: output = Path(temporary) / "result.pdf" def failed_run(*_args, **kwargs): (Path(kwargs["cwd"]) / "text.pdf").write_bytes(b"partial") return subprocess.CompletedProcess( args=[], returncode=1, stdout="! LaTeX Error: Something's wrong--perhaps a missing \\item.\n", ) console = io.StringIO() with patch.object(utils.subprocess, "run", side_effect=failed_run), \ contextlib.redirect_stdout(console): utils.compile_to_pdf("broken", output) self.assertEqual(output.read_bytes(), b"partial") self.assertIn("Warning: LaTeX compilation reported errors", console.getvalue()) self.assertIn(utils.LATEX_WARNING_MARKER + str(output.resolve()), console.getvalue()) self.assertIn("a PDF was generated", console.getvalue()) self.assertIn("missing \\item", console.getvalue()) def test_says_no_pdf_was_generated_when_latex_fails(self): with tempfile.TemporaryDirectory() as temporary: output = Path(temporary) / "result.pdf" result = subprocess.CompletedProcess( args=[], returncode=1, stdout="! Undefined control sequence.\n" ) console = io.StringIO() with patch.object(utils.subprocess, "run", return_value=result), \ contextlib.redirect_stdout(console): utils.compile_to_pdf("broken", output) self.assertFalse(output.exists()) self.assertIn("no PDF was generated", console.getvalue()) self.assertIn("Undefined control sequence", console.getvalue()) def test_warns_when_no_pdf_is_produced(self): with tempfile.TemporaryDirectory() as temporary: output = Path(temporary) / "result.pdf" result = subprocess.CompletedProcess(args=[], returncode=0, stdout="") console = io.StringIO() with patch.object(utils.subprocess, "run", return_value=result), \ contextlib.redirect_stdout(console): utils.compile_to_pdf("valid", output) self.assertFalse(output.exists()) self.assertIn("produced no PDF", console.getvalue()) self.assertIn(utils.LATEX_WARNING_MARKER + str(output.resolve()), console.getvalue()) if __name__ == "__main__": unittest.main()