SPB Git

Repository Public Research preview

test_latex_parser.py 7.7 KB 207 lines python

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
Files main
Menu

Theme