Skip to content

Commit 6bda0f0

Browse files
authored
chore: add tests for TeX output (#545)
* Add TeX integration test * Slightly better docstring * Remove unnecessary opens * Add a couple of unit tests also
1 parent 9af5f43 commit 6bda0f0

8 files changed

Lines changed: 301 additions & 2 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
_out
88
*.olean
99
/.verso/
10-
/src/tests/parser/*/*.output
10+
/src/tests/integration/**/output

‎src/tests/TestMain.lean‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Released under Apache 2.0 license as described in the file LICENSE.
44
Author: David Thrane Christiansen
55
-/
66

7+
import Verso
8+
import VersoManual
79
import VersoSearch.PorterStemmer
810
import Tests
911

@@ -29,8 +31,34 @@ def testStemmer (_ : Config) : IO Unit := do
2931
IO.eprintln s!"{x} --> {s} (wanted '{y}')"
3032
throw <| IO.userError "Stemmer tests failed"
3133

34+
/--
35+
Tests manual-genre TeX generation. `dir` is a subdirectory specific to a particular test document,
36+
which is where actual output should go, and which contains the expected output directory.
37+
`doc` is the document to be rendered.
38+
-/
39+
def testTexOutput (dir : System.FilePath) (doc : Verso.Doc.Part Verso.Genre.Manual) :
40+
Config → IO Unit := fun config =>
41+
let versoConfig : Verso.Genre.Manual.Config := {
42+
destination := "src/tests/integration" / dir / "output",
43+
emitTeX := true,
44+
emitHtmlMulti := false
45+
}
46+
47+
let runTest : IO Unit :=
48+
open Verso Genre Manual in do
49+
let logError (msg : String) := IO.eprintln msg
50+
ReaderT.run (emitTeX logError versoConfig doc) extension_impls%
51+
52+
Verso.Integration.runTests {
53+
testDir := "src/tests/integration" / dir,
54+
updateExpected := config.updateExpected,
55+
runTest
56+
}
57+
58+
open Verso.Integration in
3259
def tests := [
33-
testStemmer
60+
testStemmer,
61+
testTexOutput "sample-doc" SampleDoc.doc
3462
]
3563

3664
def getConfig (config : Config) : List String → IO Config

‎src/tests/Tests.lean‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
44
Author: David Thrane Christiansen
55
-/
66
import Tests.Golden
7+
import Tests.Integration
8+
import Tests.Integration.SampleDoc
79
import Tests.ParserRegression
10+
import Tests.TexUnit

‎src/tests/Tests/Integration.lean‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Jason Reed
5+
-/
6+
import Lean.Util.Diff
7+
8+
namespace Verso.Integration
9+
10+
/-- Configuration for the test runner -/
11+
structure Config where
12+
/-- Where are expected files located? We expect a subdirectory
13+
`expected` and `runTest` should produce files into a subdirectory
14+
`output`. -/
15+
testDir : System.FilePath
16+
/-- Should the expected output be replaced with the actual output? -/
17+
updateExpected : Bool := false
18+
/-- How to run the test -/
19+
runTest : IO Unit
20+
21+
/--
22+
Returns all non-directory filepaths that are children of `root`, which
23+
must be a directory. Returns these as paths relative to `root`.
24+
25+
This differs from `System.FilePath.walkRoot`, in that the latter returns
26+
absolute paths, and includes subdirectories.
27+
-/
28+
partial def filesBelow (root : System.FilePath) :
29+
IO (Array System.FilePath) := Prod.snd <$> StateT.run (go ".") #[]
30+
where
31+
go (p : System.FilePath) := do
32+
for d in (← (root / p).readDir) do
33+
if ← d.path.isDir then
34+
go (p / d.fileName)
35+
else
36+
modify (·.push (p / d.fileName))
37+
38+
/--
39+
Given an array of pairs `(src, tgt)` of absolute paths, copy every
40+
`src` to every `tgt`, creating directories as necessary.
41+
-/
42+
partial def copyFiles (pairs : Array (System.FilePath × System.FilePath)) :
43+
IO Unit := do
44+
for (src, tgt) in pairs do
45+
if let .some parent := tgt.parent then
46+
IO.FS.createDirAll parent
47+
IO.FS.writeBinFile tgt (← IO.FS.readBinFile src)
48+
49+
/-- Main test runner -/
50+
def runTests (config : Config) : IO Unit := do
51+
unless ← System.FilePath.pathExists config.testDir do
52+
throw <| .userError s!"Test directory not found: {config.testDir}"
53+
54+
let outputRoot := config.testDir / "output"
55+
let expectedRoot := config.testDir / "expected"
56+
57+
if config.updateExpected then
58+
let outputFiles := (← filesBelow outputRoot)
59+
IO.println s!"Updating expected outputs in {config.testDir}..."
60+
if ← System.FilePath.pathExists expectedRoot then do
61+
IO.FS.removeDirAll expectedRoot
62+
copyFiles (outputFiles.map (fun p => (outputRoot / p, expectedRoot / p)))
63+
else
64+
unless ← System.FilePath.pathExists expectedRoot do
65+
throw <| .userError s!"Expected output directory not found: {expectedRoot}"
66+
let expectedFiles := (← filesBelow expectedRoot)
67+
68+
IO.println s!"Running test in {config.testDir}..."
69+
if ← outputRoot.pathExists then
70+
IO.FS.removeDirAll outputRoot
71+
config.runTest
72+
let outputFiles := (← filesBelow outputRoot)
73+
74+
if expectedFiles != outputFiles then
75+
IO.println s!"✗ Expected files differ from actual files"
76+
IO.println s!"Expected files in {expectedRoot}:\n {expectedFiles}"
77+
IO.println s!"Actual files in {outputRoot}:\n {outputFiles}"
78+
throw <| .userError s!"Test in {config.testDir} failed"
79+
80+
for file in expectedFiles do
81+
let expected ← IO.FS.readFile (expectedRoot / file)
82+
let actual ← IO.FS.readFile (outputRoot / file)
83+
if expected != actual then
84+
let d := Lean.Diff.diff (expected.split (· == '\n') |>.toArray) (actual.split (· == '\n') |>.toArray)
85+
IO.println s!"✗ In test {config.testDir}, output file {file}"
86+
IO.println s!" Expected output differs from actual output"
87+
IO.println (Lean.Diff.linesToString d)
88+
throw <| .userError s!"Test in {config.testDir} failed"
89+
90+
return
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Jason Reed
5+
-/
6+
import Verso
7+
import VersoManual
8+
9+
namespace Verso.Integration.SampleDoc
10+
11+
open Verso Genre Manual
12+
13+
--------------------
14+
15+
/-- This is a docstring.
16+
17+
Here's some more text with a `code inline` in it.
18+
Here's when a `code inline`
19+
occurs right before a line break.
20+
21+
And then here's a paragraph break.
22+
-/
23+
def sample_constant := Unit
24+
25+
#docs (Manual) doc "Title of the Doc" :=
26+
:::::::
27+
28+
%%%
29+
shortTitle := "ShortTitle"
30+
authors := ["Harry Q. Bovik"]
31+
%%%
32+
33+
{docstring sample_constant}
34+
35+
:::::::
36+
37+
end Verso.Integration.SampleDoc

‎src/tests/Tests/TexUnit.lean‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Jason Reed
5+
-/
6+
import Tests.TexUtil
7+
8+
/-!
9+
Unit tests covering TeX output given given concrete Verso structures.
10+
-/
11+
12+
open Verso Genre.Manual
13+
14+
/--
15+
info: before\Verb|verb|
16+
after
17+
-/
18+
#guard_msgs in
19+
#eval do
20+
let b : Doc.Block Genre.Manual := .concat #[
21+
.para #[
22+
.text "before",
23+
.other (Inline.leanFromMarkdown default) #[.code "verb"],
24+
.text "after"
25+
]
26+
]
27+
IO.println (← toTex b).asString
28+
29+
/-- info: before\Verb|verb|after -/
30+
#guard_msgs in
31+
#eval do
32+
let b : Doc.Block Genre.Manual := .concat #[
33+
.para #[
34+
.text "before",
35+
.code "verb",
36+
.text "after"
37+
]
38+
]
39+
IO.println (← toTex b).asString

‎src/tests/Tests/TexUtil.lean‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Jason Reed
5+
-/
6+
import Lean.Elab.Command
7+
import VersoManual
8+
9+
/-!
10+
Support for writing unit tests covering TeX output.
11+
-/
12+
13+
open Verso Genre.Manual
14+
15+
/--
16+
Render a Verso block in Manual Genre to TeX in isolation
17+
18+
This requires the `IO` monad because `TraverseM` does.
19+
-/
20+
def toTex (block : Doc.Block Genre.Manual) : IO Output.TeX := do
21+
let extension_impls := extension_impls%
22+
23+
-- Traversal monadic data
24+
let traverseContext : TraverseContext := {
25+
logError msg := IO.println msg,
26+
}
27+
let traverseState : TraverseState := {
28+
remoteContent := {},
29+
}
30+
31+
-- Traverse the block. This shadows both `block` and `traverseState`.
32+
-- This is where we engage with the IO at the bottom of TraverseM.
33+
let ⟨block, traverseState⟩ ← Doc.Genre.traverse.block Genre.Manual block
34+
|>.run extension_impls traverseContext traverseState
35+
36+
-- Options for TeX
37+
let options : Doc.TeX.Options Genre.Manual (ReaderT ExtensionImpls IO) := {
38+
headerLevel := none,
39+
logError msg := IO.println msg,
40+
}
41+
42+
-- Convert the block to TeX
43+
block.toTeX.run ⟨options, traverseContext, traverseState⟩ |>.run extension_impls
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
\documentclass{memoir}
3+
4+
\usepackage{sourcecodepro}
5+
\usepackage{sourcesanspro}
6+
\usepackage{sourceserifpro}
7+
8+
\usepackage{fancyvrb}
9+
\usepackage{fvextra}
10+
11+
12+
\makechapterstyle{lean}{%
13+
\renewcommand*{\chaptitlefont}{\sffamily\HUGE}
14+
\renewcommand*{\chapnumfont}{\chaptitlefont}
15+
% allow for 99 chapters!
16+
\settowidth{\chapindent}{\chapnumfont 999}
17+
\renewcommand*{\printchaptername}{}
18+
\renewcommand*{\chapternamenum}{}
19+
\renewcommand*{\chapnumfont}{\chaptitlefont}
20+
\renewcommand*{\printchapternum}{%
21+
\noindent\llap{\makebox[\chapindent][l]{%
22+
\chapnumfont \thechapter}}}
23+
\renewcommand*{\afterchapternum}{}
24+
}
25+
26+
\chapterstyle{lean}
27+
28+
\setsecheadstyle{\sffamily\bfseries\Large}
29+
\setsubsecheadstyle{\sffamily\bfseries\large}
30+
\setsubsubsecheadstyle{\sffamily\bfseries}
31+
32+
\renewcommand{\cftchapterfont}{\normalfont\sffamily}
33+
\renewcommand{\cftsectionfont}{\normalfont\sffamily}
34+
\renewcommand{\cftchapterpagefont}{\normalfont\sffamily}
35+
\renewcommand{\cftsectionpagefont}{\normalfont\sffamily}
36+
37+
\title{\sffamily Title of the Doc}
38+
\author{\sffamily Harry Q. Bovik}
39+
\date{\sffamily }
40+
41+
\begin{document}
42+
43+
\frontmatter
44+
45+
\begin{titlingpage}
46+
\maketitle
47+
\end{titlingpage}
48+
49+
\tableofcontents
50+
51+
\mainmatter
52+
53+
\chapter*{Introduction}
54+
This is a docstring.Here's some more text with a \Verb|code inline|
55+
in it.
56+
Here's when a \Verb|code inline|
57+
58+
occurs right before a line break.And then here's a paragraph break.
59+
\end{document}

0 commit comments

Comments
 (0)