Repository Public Research preview
1"""Unit tests for htr.parse.latex on a synthetic arXiv-style LaTeX project (no network)."""2from __future__ import annotations34import io5import re6import tarfile7from pathlib import Path89import pytest1011from htr.parse.latex import parse_latex_archive, strip_comments12from htr.parse.protect import protect1314MAIN = r"""\documentclass{article}15\usepackage{amsmath}16\newcommand{\eg}{e.g.\xspace}17\newcommand{\be}{\begin{equation}}18\newcommand{\ee}{\end{equation}}19\newtheorem{thm}{Theorem}20\title{A Synthetic Paper on Asset Prices}21\author{Jane Doe \and John Roe}22\begin{document}23\maketitle24\begin{abstract}25We study the pricing of risky assets when investors face \emph{borrowing constraints}.26The equity premium rises by 2.5\% in our calibration.27\end{abstract}2829\section{Introduction}30\label{sec:intro}31Asset prices respond to shocks~\cite{smith2010,doe2012}. Prior work, \eg the habit model of32\citet{campbell1999}, uses an inline quantity $r_t = \log R_t$ to measure returns.% a trailing comment33% a full-line comment that must not split the paragraph34This sentence continues the same paragraph and cites \citep[see][p.~3]{fama1970}.3536As shown in Section~\ref{sec:model}, the ``equilibrium'' price satisfies37\begin{equation}38 P_t = \mathbb{E}_t\left[ M_{t+1} (P_{t+1} + D_{t+1}) \right] \label{eq:euler}39\end{equation}40which is the standard Euler equation --- see \textbf{Figure~\ref{fig:one}}.\footnote{A footnote that is dropped.}4142\input{model}4344\begin{figure}[t]45\centering46\includegraphics[width=0.5\textwidth]{fig1.pdf}47\caption{Price dynamics under the \emph{constrained} equilibrium with $\gamma = 2$.}48\label{fig:one}49\end{figure}5051\section*{Acknowledgments}52We thank the referee.5354\bibliographystyle{plain}55\begin{thebibliography}{9}56\bibitem{smith2010} A. Smith. Some paper. 2010.57\end{thebibliography}58\end{document}59"""6061MODEL = r"""\section{Model}\label{sec:model}62\subsection{Preferences}63Investors maximise expected utility $U(c) = c^{1-\gamma}/(1-\gamma)$ subject to a budget constraint that64binds in 30\% of states, as in \cite{merton1973}.6566\begin{thm}[Existence]67An equilibrium exists whenever $\gamma > 0$.68\end{thm}6970The pricing kernel follows71\[ M_{t+1} = \beta \left(\frac{C_{t+1}}{C_t}\right)^{-\gamma} \]7273\be74x = y75\ee76"""777879def _make_archive(tmp_path: Path) -> Path:80 path = tmp_path / "1501.00001.tar.gz"81 with tarfile.open(path, "w:gz") as tf:82 for name, text in (("paper.tex", MAIN), ("model.tex", MODEL), ("unused.tex", "\\section{Unused}\nNot included.\n")):83 data = text.encode()84 info = tarfile.TarInfo(name)85 info.size = len(data)86 tf.addfile(info, io.BytesIO(data))87 return path888990STUB = {91 "title": "A Synthetic Paper on Asset Prices", "authors": ["Jane Doe", "John Roe"], "author_group": "doe_j",92 "pub_date": "2015-01-06", "pub_year": 2015, "pub_date_evidence": "arxiv v1 submitted (OAI arXivRaw)",93 "license": {"code": "CC-BY", "uri": "http://creativecommons.org/licenses/by/3.0/", "evidence": "oai arXivRaw license"},94 "subject_raw": ["q-fin.GN"], "doc_type": "preprint", "source_ids": {"arxiv_id": "1501.00001"},95}969798@pytest.fixture()99def doc(tmp_path):100 return parse_latex_archive(_make_archive(tmp_path), STUB)101102103def test_metadata_from_stub(doc):104 assert doc.title == STUB["title"]105 assert doc.authors == ["Jane Doe", "John Roe"]106 assert doc.pub_date == "2015-01-06" and doc.pre2018_verified107 assert doc.license_code == "CC-BY" and doc.language is None108109110def test_roles_and_sections(doc):111 roles = [p.section_role for p in doc.paragraphs]112 assert roles[0] == "abstract"113 assert "introduction" in roles and "theory" in roles and "caption" in roles114 titles = {p.section_title for p in doc.paragraphs}115 assert "Preferences" in titles # subsection from the \input file116 pref = next(p for p in doc.paragraphs if p.section_title == "Preferences")117 assert pref.section_depth == 2 and pref.section_role == "theory" # inherited from "Model"118 assert not any("referee" in p.text for p in doc.paragraphs) # acknowledgements excluded119 assert not any("Some paper" in p.text for p in doc.paragraphs) # bibliography dropped120 assert not any("Unused" in (p.section_title or "") for p in doc.paragraphs) # file not \input121122123def test_citations_bracketed(doc):124 intro = [p for p in doc.paragraphs if p.section_role == "introduction"]125 first = intro[0]126 assert "[smith2010,doe2012]" in first.text127 assert "[campbell1999]" in first.text and "[fama1970]" in first.text128 assert "[smith2010,doe2012]" in first.citations and "has_citation" in first.flags129 assert "(ref)" in intro[1].text130131132def test_comments_do_not_split_paragraph(doc):133 first = next(p for p in doc.paragraphs if p.section_role == "introduction")134 assert "This sentence continues the same paragraph" in first.text135 assert "comment" not in first.text136137138def test_math_preserved(doc):139 first = next(p for p in doc.paragraphs if p.section_role == "introduction")140 assert "$r_t = \\log R_t$" in first.text141 euler = next(p for p in doc.paragraphs if "Euler" in p.text)142 assert "$$P_t = \\mathbb{E}_t\\left[ M_{t+1} (P_{t+1} + D_{t+1}) \\right]$$" in euler.text143 assert "\\label" not in euler.text and "has_disp_formula" in euler.flags144 assert "—" in euler.text and "“equilibrium”" in euler.text145 assert "footnote" not in euler.text146 kernel = next(p for p in doc.paragraphs if "pricing kernel" in p.text)147 assert "$$M_{t+1} = \\beta" in kernel.text148 assert "$$x = y$$" in kernel.text # display-only block (user macro \be) attached to the previous paragraph149 assert protect(kernel.text).math150151152def test_caption_and_theorem(doc):153 cap = next(p for p in doc.paragraphs if p.section_role == "caption")154 assert cap.text.startswith("Price dynamics under the constrained equilibrium")155 assert "$\\gamma = 2$" in cap.text156 thm = next(p for p in doc.paragraphs if p.text.startswith("Theorem"))157 assert thm.text.startswith("Theorem (Existence). An equilibrium exists")158159160def test_macros_and_specials(doc):161 first = next(p for p in doc.paragraphs if p.section_role == "introduction")162 assert "e.g. the habit model" in first.text163 assert doc.paragraphs[0].text.endswith("constraints.") or "borrowing constraints" in doc.paragraphs[0].text164 assert any("2.5%" in p.text for p in doc.paragraphs)165 assert any("30% of states" in p.text for p in doc.paragraphs)166 for p in doc.paragraphs:167 prose = re.sub(r"\$\$[^$]+\$\$|\$[^$]+\$", "", p.text)168 assert "\\" not in prose and "{" not in prose, prose169170171def test_offsets_consistent(doc):172 for p in doc.paragraphs:173 assert doc.body_text[p.char_start:p.char_end] == p.text174 assert doc.body_text.endswith("\n\n")175 assert [p.para_index for p in doc.paragraphs] == list(range(len(doc.paragraphs)))176177178def test_warnings(doc):179 assert any("bibliography" in w for w in doc.warnings)180 assert any("Acknowledg" in w for w in doc.warnings)181182183def test_strip_comments_escaped_percent():184 assert strip_comments("50\\% of cases % note\n% full\nnext") == "50\\% of cases \nnext"185186187def test_unsafe_member_rejected(tmp_path):188 path = tmp_path / "bad.tar.gz"189 with tarfile.open(path, "w:gz") as tf:190 for name, text in (("../evil.tex", "x"), ("main.tex", "\\documentclass{article}\\begin{document}\nHello world text.\n\\end{document}")):191 data = text.encode()192 info = tarfile.TarInfo(name)193 info.size = len(data)194 tf.addfile(info, io.BytesIO(data))195 d = parse_latex_archive(path, STUB)196 assert any("unsafe" in w for w in d.warnings)197 assert not (tmp_path.parent / "evil.tex").exists()198199200def test_pdf_only_raises(tmp_path):201 p = tmp_path / "x.gz"202 import gzip203204 p.write_bytes(gzip.compress(b"%PDF-1.4 fake"))205 with pytest.raises(ValueError, match="PDF-only"):206 parse_latex_archive(p, STUB)207