From 3abccdf072065980ceb83d8cebf7b848b3815e5e Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 13 Oct 2025 22:55:32 +0200 Subject: [PATCH] Reject PDF when citations are missing --- .../tarballs/missing-cite-biber/00README.json | 11 ++++++ .../tarballs/missing-cite-biber/foo.bib | 7 ++++ .../tarballs/missing-cite-biber/main.tex | 7 ++++ .../missing-cite-biber.tar.gz | Bin 0 -> 508 bytes .../missing-cite-bibtex/00README.json | 11 ++++++ .../tarballs/missing-cite-bibtex/foo.bib | 7 ++++ .../tarballs/missing-cite-bibtex/main.tex | 6 +++ .../missing-cite-bibtex.tar.gz | Bin 0 -> 482 bytes tex2pdf-service/tests/test_docker.py | 22 +++++++++++ .../tex2pdf/tex_to_pdf_converters.py | 35 ++++++++++++++++-- 10 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/00README.json create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/foo.bib create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/main.tex create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/missing-cite-biber.tar.gz create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/00README.json create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/foo.bib create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/main.tex create mode 100644 tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/missing-cite-bibtex.tar.gz diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/00README.json b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/00README.json new file mode 100644 index 00000000..646ef028 --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/00README.json @@ -0,0 +1,11 @@ +{ + "sources" : [ + { + "usage" : "toplevel", + "filename" : "main.tex" + } + ], + "process" : { + "compiler" : "pdflatex" + } +} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/foo.bib b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/foo.bib new file mode 100644 index 00000000..36a7fa67 --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/foo.bib @@ -0,0 +1,7 @@ +@MISC{abc, + author = "Someone", + title = {Some Title}, + howpublished = {It got published!}, + year = {2025}, + note = {Betelgeuse} +} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/main.tex b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/main.tex new file mode 100644 index 00000000..a8814cfd --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/main.tex @@ -0,0 +1,7 @@ +\documentclass{article} +\usepackage[backend=bibtex]{biblatex} +\addbibresource{foo.bib} +\begin{document} +(Type your content here.) \cite{abc} Hell WOrld \cite{foobar} So we go +\printbibliography +\end{document} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/missing-cite-biber.tar.gz b/tex2pdf-service/tests/fixture/tarballs/missing-cite-biber/missing-cite-biber.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..9f6f741ef2f29d540076c6f41752fa1730e95ea6 GIT binary patch literal 508 zcmVb_$%5cjNU#@=}~L6>oT=Dl#fci zY2AJ^ZEmZzP%E5dZiSWToP&45yC{)!yss72J%l)bNDvbqQCk5z6`l y2wfZAHrsIJj&{`O(ich)1VIo4K@bE%5ClOG1VIo4K@bFSA$|iQ)d^kzC;$MmI{k3~ literal 0 HcmV?d00001 diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/00README.json b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/00README.json new file mode 100644 index 00000000..646ef028 --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/00README.json @@ -0,0 +1,11 @@ +{ + "sources" : [ + { + "usage" : "toplevel", + "filename" : "main.tex" + } + ], + "process" : { + "compiler" : "pdflatex" + } +} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/foo.bib b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/foo.bib new file mode 100644 index 00000000..36a7fa67 --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/foo.bib @@ -0,0 +1,7 @@ +@MISC{abc, + author = "Someone", + title = {Some Title}, + howpublished = {It got published!}, + year = {2025}, + note = {Betelgeuse} +} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/main.tex b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/main.tex new file mode 100644 index 00000000..dfa5c4af --- /dev/null +++ b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/main.tex @@ -0,0 +1,6 @@ +\documentclass{article} +\begin{document} +(Type your content here.) \cite{abc} Hell WOrld \cite{foobar} so it goes. +\bibliographystyle{plain} +\bibliography{foo} +\end{document} diff --git a/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/missing-cite-bibtex.tar.gz b/tex2pdf-service/tests/fixture/tarballs/missing-cite-bibtex/missing-cite-bibtex.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fb375d6f317fc89ac8754725b5d84bd7cecad733 GIT binary patch literal 482 zcmV<80UiDyiwFP!000001MSvJi=03Z0ASDlii)QM7HJ(H5D4V8kUcDc>_QHiJ+xY* zhSrwuHi?M;J>6y|%pSs=W+w^tU7D%thsad5jD5O(^Zxca{HkT|od>hBBynSur|fuK z#hcuVQkHMBge6Jlu_(^-$OCq!=Xqq*hARNCm#RQz{y6Vc{vS2_01#+7suFboFX4*| zSo3~#=~43gF#7q%6iOU`=FHF zVArd4@lfpqKP+DS#lQ6H`S0zd42!ln>lr8OKVq)_vn*$69B*9x$8kpW{}f)|-rT*L zc~QCw<)dk&f>#jS$qr?YYnhwY2(-c31>xTPT3o!5--odft!}V#=^F#JH1Iq2Y@IbB zcRS88i_=RD(N{ipl;KTgw)?FF{}lZ5X7C$X($Id<5a zQgE$ju1s6n`u4X4)@?r@vWx%xei{%a`=J1(?2U~a^jDK7oG2qA^<16H(WQ_5dgV0Hwn6Y5)KL literal 0 HcmV?d00001 diff --git a/tex2pdf-service/tests/test_docker.py b/tex2pdf-service/tests/test_docker.py index 34eb7b44..3b14476c 100644 --- a/tex2pdf-service/tests/test_docker.py +++ b/tex2pdf-service/tests/test_docker.py @@ -741,3 +741,25 @@ def test_first_line(docker_container, ts): meta, status = submit_tarball(url, tarball, outcome, api_args={"auto_detect": "true", "ts": ts}) assert meta is not None assert meta.get("pdf_file") == "first-line.pdf" + + +@pytest.mark.integration +def test_missing_cite_bibtex(docker_container): + url = docker_container + "/convert" + tarball = os.path.join(SELF_DIR, "fixture/tarballs/missing-cite-bibtex/missing-cite-bibtex.tar.gz") + outcome = os.path.join(SELF_DIR, "output/missing-cite-bibtex.outcome.tar.gz") + meta, status = submit_tarball(url, tarball, outcome, api_args={"auto_detect": "false"}) + assert meta is not None + # compilation must fail on missing citations + assert meta.get("status") == "fail" + + +@pytest.mark.integration +def test_missing_cite_biber(docker_container): + url = docker_container + "/convert" + tarball = os.path.join(SELF_DIR, "fixture/tarballs/missing-cite-biber/missing-cite-biber.tar.gz") + outcome = os.path.join(SELF_DIR, "output/missing-cite-biber.outcome.tar.gz") + meta, status = submit_tarball(url, tarball, outcome, api_args={"auto_detect": "false"}) + assert meta is not None + # compilation must fail on missing citations + assert meta.get("status") == "fail" diff --git a/tex2pdf-service/tex2pdf/tex_to_pdf_converters.py b/tex2pdf-service/tex2pdf/tex_to_pdf_converters.py index ac16a6b4..2c0c62ec 100644 --- a/tex2pdf-service/tex2pdf/tex_to_pdf_converters.py +++ b/tex2pdf-service/tex2pdf/tex_to_pdf_converters.py @@ -277,6 +277,7 @@ def _run_base_engine_necessary_times( logger.warning("Last run had changing labels, but we exhausted the MAX_LATEX_RUNS limit.") else: status = "fail" + citation_missing = False for line in run["log"].splitlines(): for error_needle, error_msg in error_needles: if error_needle.search(line): @@ -298,7 +299,7 @@ def _run_base_engine_necessary_times( ) return outcome for rerun_needle in rerun_needles: - if line.find(rerun_needle) >= 0: + if rerun_needle.search(line): # Need retry logger.debug(f"Found rerun needle {rerun_needle}") if iteration == iteration_list[-1]: @@ -308,11 +309,35 @@ def _run_base_engine_necessary_times( else: status = "fail" break + if MISSING_CITE_RE.search(line): + logger.debug(f"MISSING_CITE: {MISSING_CITE_RE} found in line {line}") + citation_missing = True + else: + logger.debug(f"MISSING_CITE: {MISSING_CITE_RE} not found in line {line}") run["iteration"] = iteration outcome.update({"runs": self.runs, "status": status, "step": step}) if status == "success": - break - + # if no rerun needle is found, we would return now, but there might + # be still unresolved references. In this case, error out. + logger.debug(f"Checking for citation_missing = {citation_missing}") + if citation_missing: + # Note! We need to delete the PDF/DVI file otherwise "upstream" Converter + # believes all is fine and continues with success! + name = run[base_format]["name"] + artifact_file = os.path.join(in_dir, name) + if os.path.exists(artifact_file): + logger.debug("Output %s deleted due to failed run", name) + os.unlink(artifact_file) + run[base_format] = file_props(artifact_file) + outcome.update( + { + "status": "fail", + "reason": "Missing citation reference.", + } + ) + return outcome + else: + break return outcome def _exec_cmd( @@ -623,8 +648,10 @@ def select_converter_class(zzrm: ZeroZeroReadMe | None) -> type[BaseConverter]: bad_for_pdftex_packages = {pname: True for pname in ["fontspec"]} bad_for_tex_packages = {pname: True for pname in ["fontspec"]} +# Thanks biblatex to slightly change the warning message from a ` to a ' ... wonderful trick! +MISSING_CITE_RE = re.compile("LaTeX Warning: Citation [`'].*' on page [0-9]* undefined on input line [0-9]*\.") rerun_needles = [ - "Rerun to get cross-references right\.", + re.compile("Rerun to get cross-references right\."), ] error_needles = [ (