diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9e31734d8..310825ff4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,6 +13,9 @@ jobs:
build:
name: Build and test
runs-on: nscloud-ubuntu-22.04-amd64-8x16
+ permissions:
+ contents: read
+ checks: write
env:
# Used for browser tests. Placing them here allows caching to work right.
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
@@ -149,7 +152,34 @@ jobs:
- name: Run tests
run: |
- lake test -- --verbose --check-tex
+ lake test -- --test-options --verbose --wfail --junit=errata-report.xml --markdown=errata-summary.md -- --check-tex
+
+ - name: Add test results to the job summary
+ if: always()
+ run: |
+ if [ -f errata-summary.md ]; then cat errata-summary.md >> "$GITHUB_STEP_SUMMARY"; fi
+
+ # Publishing a check run needs a token with `checks: write`, which pull requests from
+ # forks never receive; those runs get their results from the job summary and the
+ # uploaded artifact instead.
+ - name: Publish the JUnit test report
+ if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
+ uses: dorny/test-reporter@v1
+ with:
+ name: Errata tests
+ path: errata-report.xml
+ reporter: java-junit
+ fail-on-error: false
+
+ - name: Upload the raw test reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: errata-test-reports
+ path: |
+ errata-report.xml
+ errata-summary.md
+ if-no-files-found: ignore
- name: Test the dev server
run: |
diff --git a/.github/workflows/test-imports.yml b/.github/workflows/test-imports.yml
index 85c778c5c..cc210987c 100644
--- a/.github/workflows/test-imports.yml
+++ b/.github/workflows/test-imports.yml
@@ -1,3 +1,8 @@
+# This check is retained only to satisfy the required "Check all test modules are imported" status
+# check on the protected branch. It is a no-op on this layout: test modules now live under
+# src/tests/VersoTests and are discovered by globbing, and src/tests/Tests no longer exists, so the
+# scan below finds nothing. Coverage is instead enforced by the test driver's unreachable-modules
+# warning during `lake test`. Remove this workflow (and drop the required check) once merged.
name: All test modules imported
on: [pull_request, merge_group]
diff --git a/.github/workflows/update-subverso.yml b/.github/workflows/update-subverso.yml
index 4e23fef9e..aff17aa12 100644
--- a/.github/workflows/update-subverso.yml
+++ b/.github/workflows/update-subverso.yml
@@ -84,7 +84,7 @@ jobs:
- name: Run tests
if: steps.check-changes.outputs.changed == 'true'
run: |
- lake test -- --verbose --check-tex
+ lake test -- --test-options --verbose --wfail -- --check-tex
- name: Create branch and open PR
if: steps.check-changes.outputs.changed == 'true'
diff --git a/doc/UsersGuide/Releases/Entries/TestFramework.lean b/doc/UsersGuide/Releases/Entries/TestFramework.lean
index 23b46293b..ad3d4a74a 100644
--- a/doc/UsersGuide/Releases/Entries/TestFramework.lean
+++ b/doc/UsersGuide/Releases/Entries/TestFramework.lean
@@ -29,3 +29,5 @@ Each test's docstring and source range are saved for failure reporting.
The test runner discovers every test in the package; it can restrict the run to named libraries, rerun property tests with a fixed seed, update golden files, fail the run on warnings with `--wfail`, and write JUnit XML, JSON, and Markdown reports.
Elaboration-time tests can be written with `#test_msgs` and `#test_guard`, variants of `#guard_msgs` and `#guard` that run their check at compile time and record the outcome as a test case, reported together with the rest of the suite.
+
+Verso's own test suite runs on Errata: `lake test` discovers and runs every test in the package, and CI publishes the resulting reports.
diff --git a/lakefile.lean b/lakefile.lean
index 180a89fcf..06dd7cf2d 100644
--- a/lakefile.lean
+++ b/lakefile.lean
@@ -135,18 +135,16 @@ lean_exe «verso-literate-plan» where
srcDir := "src/verso-literate-plan"
supportInterpreter := true
+-- All test code: Errata test modules, compile-time tests, fixtures, and generators. Submodules are
+-- globbed so each is built and every `@[test]` module is discoverable.
@[default_target]
-lean_lib Tests where
+lean_lib VersoTests where
srcDir := "src/tests"
-
-@[test_driver]
-lean_exe «verso-tests» where
- root := `TestMain
- srcDir := "src/tests"
- supportInterpreter := true
+ roots := #[`VersoTests]
+ globs := #[Glob.andSubmodules `VersoTests]
-- Everything below is Errata's own implementation: its library, its self-tests, the generated
--- discovery runner, and the runner script.
+-- discovery runner, and the `lake test` driver.
namespace Errata
@[default_target]
@@ -249,12 +247,13 @@ private def splitArgs (args : List String) : Except String (List String × List
| some opt =>
.error s!"unexpected option '{opt}': arguments before the `--test-options` marker name the \
libraries to test. Put runner options after the marker, \
- e.g. `lake run Errata.run --test-options {opt}`."
+ e.g. `lake test -- --test-options {opt}`."
| none => .ok (names, rest)
-/-- Usage information for `lake run Errata.run`. -/
+/-- Usage information for `lake test`. -/
private def usage : String := include_str "src/errata/Errata/usage.txt"
+@[test_driver]
script run (args) do
let ws ← getWorkspace
-- Answer the driver's own `--help` before discovering or building anything. A `--help` after the
diff --git a/src/errata/Errata/usage.txt b/src/errata/Errata/usage.txt
index a9ffa278e..bfaa9344c 100644
--- a/src/errata/Errata/usage.txt
+++ b/src/errata/Errata/usage.txt
@@ -1,12 +1,12 @@
Errata test runner
Usage:
- lake run Errata.run run every test in the package
- lake run Errata.run LIBRARY... run the tests in the given libraries
- lake run Errata.run LIBRARY... --test-options OPTION... pass runner options after the marker
+ lake test run every test in the package
+ lake test -- LIBRARY... run the tests in the given libraries
+ lake test -- LIBRARY... --test-options OPTION... pass runner options after the marker
Tokens before `--test-options` name libraries. A library is a bare `Library` in this package or a
`package/Library` reaching into a dependency. Everything after the marker goes to the test runner.
The runner documents its own options, including how to pass options to the tests themselves:
- lake run Errata.run --test-options --help
+ lake test -- --test-options --help
diff --git a/src/tests/TestMain.lean b/src/tests/TestMain.lean
deleted file mode 100644
index f24dc9638..000000000
--- a/src/tests/TestMain.lean
+++ /dev/null
@@ -1,415 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-
-import Verso
-import VersoManual
-import VersoSearch.PorterStemmer
-import VersoUtil.LzCompress
-import VersoLiterate
-import Tests
-
-structure Config where
- verbose : Bool := false
- updateExpected : Bool := false
- checkTeX : Bool := false
-
-open Verso.Search.Stemmer.Porter in
-def testStemmer (_ : Config) : IO Unit := do
- let voc := include_str "stemmer/voc.txt"
- let output := include_str "stemmer/output.txt"
-
- let data := voc.splitOn "\n"
- let outData := output.splitOn "\n"
-
- let mut failures := #[]
- for x in data, y in outData do
- let s := porterStem x
- unless s == y do
- failures := failures.push (x, s, y)
- unless failures.isEmpty do
- IO.eprintln s!"{failures.size} failures"
- for (x, s, y) in failures do
- IO.eprintln s!"{x} --> {s} (wanted '{y}')"
- throw <| IO.userError "Stemmer tests failed"
-
-/--
-Tests manual-genre TeX generation. `dir` is a subdirectory specific to a particular test document,
-which is where actual output should go, and which contains the expected output directory.
-`doc` is the document to be rendered.
--/
-def testTexOutput
- (dir : System.FilePath)
- (doc : Verso.Doc.VersoDoc Verso.Genre.Manual)
- (config : Config)
- (twoside : Bool := false)
- (extraFiles : List (System.FilePath × String) := [])
- (extraFilesTeX : List (System.FilePath × String) := []) : IO Unit := do
- let versoConfig : Verso.Genre.Manual.Config := {
- destination := "src/tests/integration" / dir / "output",
- emitTeX := true,
- emitHtmlMulti := .no,
- twoside,
- extraFiles,
- extraFilesTeX
- }
-
- let runTest : IO Unit :=
- open Verso Genre Manual in do
- let logger ← Verso.Logger.new
- emitTeX versoConfig doc.toPart |>.run extension_impls% |>.run logger
-
- Verso.Integration.runTests { config with
- testDir := "src/tests/integration" / dir,
- updateExpected := config.updateExpected,
- runTest
- }
-
-def testZip (cfg : Config) : IO Unit := do
- IO.println "Running zip tests with fixed files..."
- testExtract #[] .store
- testExtract #[] .deflate
- testExtract #[("empty", .empty)] .store
- testExtract #[("empty", .empty)] .deflate
- testExtract files .store
- testExtract files .deflate
- let chunkSize := me.size / 10
- for i in (0 : Nat)...10 do
- let me := me.extract 0 (i * chunkSize)
- testExtract #[("T2.lean", me)] .store
- testExtract #[("T2.lean", me)] .deflate
- for i in (0 : Nat)...10 do
- let me := me.extract 0 (i * chunkSize)
- let bwd := bwd.extract 0 (i * chunkSize)
- testExtract #[("T2.lean", me), ("other", bwd)] .store
- testExtract #[("T2.lean", me), ("other", bwd)] .deflate
- for _ in (0 : Nat)...10 do
- let seedValue ← IO.monoNanosNow
- if cfg.verbose then IO.println s!"Seed is {seedValue}"
- IO.setRandSeed seedValue
- let mut randFiles := #[]
- for _ in 0...(← IO.rand 0 15) do
- let name ← randName
- let size ← IO.rand 0 50000
- let content ← IO.getRandomBytes <| .ofNat size
- randFiles := randFiles.push (name, content)
- if cfg.verbose then
- IO.println s!"Running random zip test with {randFiles.size} files, sizes:"
- for (x, y) in randFiles do
- IO.println s!" * {x}: {y.size} bytes"
- else
- IO.println s!"Running random zip test with {randFiles.size} files"
- testExtract randFiles .store
- testExtract randFiles .deflate
-
-where
- files := #[("x.txt", "abcdef\nlkjlkj".toByteArray), ("y.txt", "".toByteArray), ("z.txt", "abc\n\n".toByteArray)]
- me := (include_str "TestMain.lean").toByteArray
- bwd := me.foldl (init := .empty) fun x y => ByteArray.empty.push y ++ x
- randName : IO String := do
- let len ← IO.rand 1 10
- let stem ← len.foldM (init := "") fun _ _ acc => do
- return acc.push <| Char.ofNat ('a'.toNat + (← IO.rand 0 25))
- let len ← IO.rand 2 4
- let ext ← len.foldM (init := "") fun _ _ acc => do
- return acc.push <| Char.ofNat ('a'.toNat + (← IO.rand 0 25))
- return stem ++ "." ++ ext
-
-open Verso.LzCompress in
-def testLz (_ : Config) : IO Unit := do
- let actual := lzCompress r#"import Mathlib.Logic.Basic -- basic facts in logic
--- theorems in Lean's mathematics library
-
--- Let P and Q be true-false statements
-variable (P Q : Prop)
-
--- The following is a basic result in logic
-example : ¬ (P ∧ Q) ↔ ¬ P ∨ ¬ Q := by
- -- its proof is already in Lean's mathematics library
- exact not_and_or
-
--- Here is another basic result in logic
-example : ¬ (P ∨ Q) ↔ ¬ P ∧ ¬ Q := by
- apply? -- we can search for the proof in the library
- -- we can also replace `apply?` with its output
-"#
- let expected :=
- "JYWwDg9gTgLgBAWQIYwBYBtgCMB0AZCAc2AGMcAhJAZ1LgFo64traAzJEmKuYAOznRFSAKAZw0AU2gSQ3" ++
- "PnDwSkvAOTcQKVDJSlumLFCRQAnsNGNF8AApxlAEzgBFJhPFQArhLrt0VV1RgUGQleLmEANyNgJCx0VwA" ++
- "KG2cALjgrKAgwAEozMQAVLThWCHRBAHc+Qh5uJCYWEjgoCSp3dHh5QWISYQkADyRwOLhUgBq4RLhAciIn" ++
- "LLhAFMI4MZtACiJFp2GAXiZTOHpGYC44MAyIVmrbdCakO2MefkVlNTgNSRfdAWxDE2Fdvo54XgQGAAfXs" ++
- "wOguUYAAkJE1zsogVooHUaA0mi02ncBEJun9Bq5RuMVjN5msbNMxiktlgdrYwGB0MYAPx7OBlVwkZRwPx" ++
- "GEioIrQcSFY4QU5YyQfAxGWlidlwTn8JC+CCNCQMjiuAAGSHpjKZmrZB35B24EHcMDA5uEQA"
- if actual ≠ expected then
- throw <| IO.userError "Mismatched lzCompress output"
-
-def testSerialization (_ : Config) : IO Unit := do
- IO.println "Running serialization tests with Plausible..."
- let fails ← runSerializationTests
- if fails > 0 then
- throw <| IO.userError s!"{fails} serialization tests failed"
-
-def testSearchJs (_ : Config) : IO Unit := do
- IO.println "Running search JS wire-format tests..."
- let fails ← Verso.Tests.SearchJs.runSearchJsTests
- if fails > 0 then
- throw <| IO.userError s!"{fails} search JS tests failed"
-
-def testBlog (_ : Config) : IO Unit := do
- IO.println "Running blog tests with Plausible..."
- let fails ← runBlogTests
- if fails > 0 then
- throw <| IO.userError s!"{fails} blog tests failed"
-
-def testServe (_ : Config) : IO Unit := do
- IO.println "Running serve tests..."
- let fails ← Verso.Tests.Serve.runServeTests
- if fails > 0 then
- throw <| IO.userError s!"{fails} serve tests failed"
-
-def testLiterateConfig (_ : Config) : IO Unit := do
- let fails ← Tests.LiterateConfig.runLiterateConfigTests
- if fails > 0 then
- throw <| IO.userError s!"{fails} literate config tests failed"
-
-def testLiterateHtml (_ : Config) : IO Unit :=
- Tests.LiterateHtml.testLiterateHtml
-
-def testLiterateHtmlMultiRoot (_ : Config) : IO Unit :=
- Tests.LiterateHtml.testLiterateHtmlMultiRoot
-
--- Interactive tests via the LSP server
-def testInteractive (_ : Config) : IO Unit := do
- IO.println "Running interactive (LSP) tests..."
- IO.println s!"current dir: {(← IO.Process.getCurrentDir)}"
- -- We use the lower-level Process.spawn, which causes the subprocess to inherit the stdio
- let child ← IO.Process.spawn { cmd := "src/tests/interactive/run_interactive.sh" }
- let exitCode ← child.wait
- if exitCode != 0 then
- throw <| IO.userError s!"Interactive LSP tests failed with exit code {exitCode}"
-
-private def hasSubstring (s : String) (sub : String) : Bool :=
- s.find? sub |>.isSome
-
-def testSetupLiterate (_ : Config) : IO Unit := do
- IO.println "Running setup-literate tests..."
- let versoRoot ← IO.FS.realPath "."
- IO.FS.withTempDir fun tmpDir => do
- let run (cmd : String) (args : Array String) : IO Unit := do
- let result ← IO.Process.output {
- cmd := cmd
- args := args
- cwd := some tmpDir.toString
- }
- if result.exitCode != 0 then
- throw <| IO.userError s!"{cmd} failed: {result.stderr}"
-
- -- Set up a project that depends on the Verso being tested
- run "git" #["init", "-q"]
- let toolchain ← IO.FS.readFile "lean-toolchain"
- IO.FS.writeFile (tmpDir / "lean-toolchain") toolchain
- IO.FS.writeFile (tmpDir / "lakefile.toml")
- s!"name = \"test-project\"\n\n[[require]]\nname = \"verso\"\npath = \"{versoRoot}\"\n"
-
- -- Test 1: Fresh generation via lake exe
- let result ← IO.Process.output {
- cmd := "lake"
- args := #["exe", "verso", "setup-literate"]
- cwd := some tmpDir.toString
- }
- if result.exitCode != 0 then
- throw <| IO.userError s!"setup-literate failed: {result.stderr}\n{result.stdout}"
-
- let workflowFile := tmpDir / ".github" / "workflows" / "verso-literate-pages.yml"
- unless ← workflowFile.pathExists do
- throw <| IO.userError "Workflow file was not created"
-
- let content ← IO.FS.readFile workflowFile
- let checks := #[
- ("lake query :literateHtml", "lake query command"),
- ("deploy-pages@v", "deploy-pages action"),
- ("upload-pages-artifact@v", "upload-pages-artifact action"),
- ("lean-action@v", "lean-action")
- ]
- for (needle, desc) in checks do
- unless hasSubstring content needle do
- throw <| IO.userError s!"Workflow file missing {desc} ({needle})"
- IO.println " fresh generation: passed"
-
- -- Test 2: Idempotent (no change)
- let result2 ← IO.Process.output {
- cmd := "lake"
- args := #["exe", "verso", "setup-literate"]
- cwd := some tmpDir.toString
- }
- unless hasSubstring result2.stdout "up to date" do
- throw <| IO.userError "Expected 'up to date' message on second run"
- IO.println " idempotent: passed"
-
- -- Test 3: Outdated file gets .bak
- IO.FS.writeFile workflowFile "modified content\n"
- let result3 ← IO.Process.output {
- cmd := "lake"
- args := #["exe", "verso", "setup-literate"]
- cwd := some tmpDir.toString
- }
- if result3.exitCode != 0 then
- throw <| IO.userError s!"setup-literate (update) failed: {result3.stderr}"
- let bakFile := tmpDir / ".github" / "workflows" / "verso-literate-pages.yml.bak"
- unless ← bakFile.pathExists do
- throw <| IO.userError ".bak file was not created when updating"
- let bakContent ← IO.FS.readFile bakFile
- unless hasSubstring bakContent "modified content" do
- throw <| IO.userError ".bak file should contain old content"
- IO.println " backup on update: passed"
-
- IO.println " All setup-literate tests passed."
-
-open Verso in
-def testBuildLog (_ : Config) : IO Unit := do
- IO.println "Running build-log tests..."
- -- A message logged with a position is saved with that location (a location always names a file).
- let logger ← Logger.new
- let pos : Lean.Lsp.Position := { line := 4, character := 2 }
- (reportError "boom" (some { file := "PosSave.lean", span := .pos pos }) : BuildLogT IO Unit).run logger
- let errs ← logger.errors
- let some m := errs[0]?
- | throw <| IO.userError s!"expected 1 saved error, got {errs.size}"
- unless m.severity == .error do throw <| IO.userError "expected error severity"
- match m.loc with
- | some { file := "PosSave.lean", span := .pos p } =>
- unless p.line == 4 && p.character == 2 do
- throw <| IO.userError "saved position does not match the logged span"
- | _ => throw <| IO.userError "expected a saved `PosSave.lean` `pos` location"
-
- -- A `range` span is likewise saved.
- let logger2 ← Logger.new
- let r : Lean.Lsp.Range :=
- { start := { line := 1, character := 0 }, «end» := { line := 1, character := 5 } }
- (reportWarning "careful" (some { file := "RangeSave.lean", span := .range r }) : BuildLogT IO Unit).run logger2
- let some w := (← logger2.warnings)[0]?
- | throw <| IO.userError "expected 1 saved warning"
- match w.loc with
- | some { span := .range _, .. } => pure ()
- | _ => throw <| IO.userError "expected a `range` span to be saved"
-
- -- Range formatting defers to Lean's `mkErrorStringWithPos`: `file:line:col-line:col`, 1-based
- -- line, 0-based column, with the full end position even within one line (no `line:col-col` collapse).
- let crossLine : LogMessage :=
- { severity := .error, text := "msg",
- loc := some { file := "CrossLine.lean",
- span := .range { start := { line := 19, character := 4 }, «end» := { line := 20, character := 7 } } } }
- unless crossLine.format == "CrossLine.lean:20:4-21:7: msg" do
- throw <| IO.userError s!"cross-line range formatted as \"{crossLine.format}\""
- let sameLine : LogMessage :=
- { severity := .error, text := "msg",
- loc := some { file := "SameLine.lean",
- span := .range { start := { line := 42, character := 4 }, «end» := { line := 42, character := 21 } } } }
- unless sameLine.format == "SameLine.lean:43:4-43:21: msg" do
- throw <| IO.userError s!"same-line range formatted as \"{sameLine.format}\""
-
- -- A located message is formatted uniformly as `file:line:col: text`.
- let loggerF ← Logger.new
- let errBufF ← IO.mkRef ({} : IO.FS.Stream.Buffer)
- IO.withStderr (IO.FS.Stream.ofBuffer errBufF) <|
- (reportError "bad term" (some { file := "FileLoc.lean", span := .pos { line := 6, character := 3 } })
- : BuildLogT IO Unit).run loggerF
- let some mF := (← loggerF.errors)[0]?
- | throw <| IO.userError "expected 1 saved error with a file location"
- unless mF.loc.map (·.file) == some "FileLoc.lean" do
- throw <| IO.userError "saved location should carry the filename"
- unless hasSubstring (String.fromUTF8! (← errBufF.get).data) "FileLoc.lean:7:3: bad term" do
- throw <| IO.userError "a file location should format as file:line:col:"
-
- -- A single logging action can emit both severities; errors set the exit code, warnings do not.
- let logger3 ← Logger.new
- (do reportError "e1"; reportWarning "w1"; reportError "e2" : BuildLogT IO Unit).run logger3
- unless (← logger3.errors).size == 2 do throw <| IO.userError "expected 2 errors"
- unless (← logger3.warnings).size == 1 do throw <| IO.userError "expected 1 warning"
- unless (← logger3.exitCode) == 1 do throw <| IO.userError "errors must yield a non-zero exit code"
-
- let logger4 ← Logger.new
- (reportWarning "just a warning" : BuildLogT IO Unit).run logger4
- unless (← logger4.exitCode) == 0 do
- throw <| IO.userError "warnings must not affect the exit code"
-
- -- Logging prints to the *ambient* stderr, resolved at log time: a logger created before a
- -- stderr redirection still writes into the redirected stream, and nothing goes to stdout.
- let logger5 ← Logger.new
- let outBuf ← IO.mkRef ({} : IO.FS.Stream.Buffer)
- let errBuf ← IO.mkRef ({} : IO.FS.Stream.Buffer)
- IO.withStdout (IO.FS.Stream.ofBuffer outBuf) <|
- IO.withStderr (IO.FS.Stream.ofBuffer errBuf) <|
- (do
- reportError "first problem" (some { file := "X.lean", span := .pos { line := 0, character := 0 } })
- reportWarning "second problem" : BuildLogT IO Unit).run logger5
- let errText := String.fromUTF8! (← errBuf.get).data
- let outText := String.fromUTF8! (← outBuf.get).data
- unless hasSubstring errText "X.lean:1:0: first problem" do
- throw <| IO.userError s!"stderr buffer is missing the formatted error; got: {errText}"
- unless hasSubstring errText "second problem" do
- throw <| IO.userError s!"stderr buffer is missing the warning; got: {errText}"
- unless outText.isEmpty do
- throw <| IO.userError s!"logging must not write to stdout; got: {outText}"
- unless (← logger5.errors).size == 1 && (← logger5.warnings).size == 1 do
- throw <| IO.userError "redirected logging should still accumulate into the logger's buffers"
- IO.println " All build-log tests passed."
-
-/-- Runs Errata's own tests, reporting them the way the Errata runner does. -/
-def testErrata (config : Config) : IO Unit := do
- let verbosity := if config.verbose then Errata.Verbosity.quiet else .silent
- let cfg ← Errata.mkContext (updateGolden := config.updateExpected)
- let results ← Errata.run cfg errataTests
- let failures ← Errata.humanReport verbosity results
- unless failures == 0 do
- throw <| IO.userError s!"{failures} Errata test(s) failed"
-
-open Verso.Integration in
-def tests := [
- testBuildLog,
- testSerialization,
- testSearchJs,
- testBlog,
- testServe,
- testStemmer,
- testTexOutput "sample-doc" SampleDoc.doc,
- testTexOutput "inheritance-doc" InheritanceDoc.doc,
- testTexOutput "code-content-doc" CodeContent.doc,
- testTexOutput "extra-files-doc" ExtraFilesDoc.doc
- (extraFiles := [("src/tests/integration/extra-files-doc/test-data/shared", "shared")])
- (extraFilesTeX := [("src/tests/integration/extra-files-doc/test-data/TeX-only", "TeX-only")]),
- testTexOutput "escape-doc" Escape.doc,
- testTexOutput "front-matter-doc" FrontMatter.doc,
- testTexOutput "diagram-doc" DiagramDoc.doc,
- testTexOutput "twoside-doc" TwoSideDoc.doc (twoside := true),
- testZip,
- testInteractive,
- testLiterateConfig,
- testLiterateHtml,
- testLiterateHtmlMultiRoot,
- testSetupLiterate,
- testErrata
-]
-
-def getConfig (config : Config) : List String → IO Config
- | [] => pure config
- | "--update-expected" :: args => getConfig { config with updateExpected := true } args
- | "--verbose" :: args | "-v" :: args => getConfig { config with verbose := true } args
- | "--check-tex" :: args => getConfig { config with checkTeX := true } args
- | other :: _ => throw <| IO.userError s!"Didn't understand {other}"
-
-def main (args : List String) : IO UInt32 := do
- let config ← getConfig {} args
- let mut failures := 0
- for test in tests do
- try
- test config
- catch
- | e => do
- IO.eprintln e
- failures := failures + 1
- if failures == 0 then
- IO.println "All tests passed"
- return failures
diff --git a/src/tests/Tests.lean b/src/tests/Tests.lean
deleted file mode 100644
index 52967abf0..000000000
--- a/src/tests/Tests.lean
+++ /dev/null
@@ -1,55 +0,0 @@
-/-
-Copyright (c) 2025-2026 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-import Tests.Basic
-import Tests.Elab
-import Tests.GenericCode
-import Tests.Golden
-import Tests.CommentSkipping
-import Tests.DocElabExtensions.Use
-import Tests.DocTerm
-import Tests.DocVisibility
-import Tests.DocstringMissing
-import Tests.DocstringMissingLegacy
-import Tests.HighlightedToTeX
-import Tests.ErrataSuite
-import Tests.ExpanderSignatures
-import Tests.ExpanderSignaturesLegacy
-import Tests.Html
-import Tests.HtmlEntities
-import Tests.InlineStringPositions
-import Tests.Tags
-import Tests.Integration
-import Tests.Integration.SampleDoc
-import Tests.Integration.CodeContent
-import Tests.Integration.DiagramDoc
-import Tests.Integration.Escape
-import Tests.Integration.ExtraFilesDoc
-import Tests.Integration.FrontMatter
-import Tests.Integration.InheritanceDoc
-import Tests.Integration.LeanSection
-import Tests.Integration.TwoSideDoc
-import Tests.LeanCode
-import Tests.Linters
-import Tests.Method
-import Tests.NestedTacticHtml
-import Tests.ParserRegression
-import Tests.Paths
-import Tests.PorterStemmer
-import Tests.Refs
-import Tests.SearchJs
-import Tests.ExtensionResolution
-import Tests.Serialization
-import Tests.HoverMerge
-import Tests.TeX
-import Tests.TexUnit
-import Tests.TexUtil
-import Tests.VersoBlog
-import Tests.VersoManual
-import Tests.Z85
-import Tests.Zip
-import Tests.LiterateConfig
-import Tests.LiterateHtml
-import Tests.Serve
diff --git a/src/tests/Tests/ErrataSuite.lean b/src/tests/Tests/ErrataSuite.lean
deleted file mode 100644
index ae0a5a218..000000000
--- a/src/tests/Tests/ErrataSuite.lean
+++ /dev/null
@@ -1,12 +0,0 @@
-/-
-Copyright (c) 2026 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-module
-
-public import Errata
-import all ErrataTests
-
-/-- Errata's own tests, gathered so that a driver outside the module system can run them. -/
-public def errataTests : Array Errata.TestEntry := getAllTests% "verso" ErrataTests
diff --git a/src/tests/Tests/Golden.lean b/src/tests/Tests/Golden.lean
deleted file mode 100644
index c7b70c3c6..000000000
--- a/src/tests/Tests/Golden.lean
+++ /dev/null
@@ -1,160 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-module
-
-public import Lean.Util.Diff
-
-public section
-
-namespace Verso.GoldenTest
-
-set_option linter.missingDocs true
-
-open Lean.Diff
-
-
-/-- Configuration for the test runner -/
-structure Config where
- /-- Where are input and expected files located? -/
- testDir : System.FilePath
- /-- Should the expected output be replaced with the actual output? -/
- updateExpected : Bool := false
- /-- How to test an input file's contents. -/
- runTest : String → IO String
-
-/-- Result of running a single test. -/
-inductive TestResult where
- /-- The test succeeded.-/
- | pass (name : String) : TestResult
- /-- The test was a failure. -/
- | fail (name expected actual : String) : TestResult
- /-- An error prevented the test from running. -/
- | error (name message : String) : TestResult
-
-/-- Statistics for a test run. -/
-structure TestStats where
- /-- The number of passing tests. -/
- passed : Nat := 0
- /-- The number of failing tests. -/
- failed : Nat := 0
- /-- The number of test that couldn't run. -/
- errors : Nat := 0
-
-/-- The total number of tests from a given run. -/
-def TestStats.total (stats : TestStats) : Nat :=
- stats.passed + stats.failed + stats.errors
-
-/-- Add a test result to the statistics-/
-def TestStats.add (stats : TestStats) (result : TestResult) : TestStats :=
- match result with
- | .pass _ => { stats with passed := stats.passed + 1 }
- | .fail _ _ _ => { stats with failed := stats.failed + 1 }
- | .error _ _ => { stats with errors := stats.errors + 1 }
-
-/-- A single test consists of three paths -/
-structure TestPaths where
- /-- The file to parse -/
- input : System.FilePath
- /-- The expected result -/
- expected : System.FilePath
- /-- The actual result -/
- output : System.FilePath
-
-/-- Get paths for a test given the input file path -/
-def getTestPaths (testDir : System.FilePath) (testName : String) : TestPaths where
- input := testDir / (testName ++ ".input")
- expected := testDir / (testName ++ ".expected")
- output := testDir / (testName ++ ".output")
-
-/-- Run a single test -/
-def runSingleTest (config : Config) (testName : String) : IO TestResult := do
- let {input, expected, output} := getTestPaths config.testDir testName
-
- try
- let inputString ← IO.FS.readFile input
- let outputString ← config.runTest inputString
- IO.FS.writeFile output outputString
-
- if config.updateExpected then
- IO.FS.writeFile expected outputString
- return TestResult.pass testName
- else
- if ← System.FilePath.pathExists expected then
- let expectedString ← IO.FS.readFile expected
- if outputString == expectedString then
- return TestResult.pass testName
- else
- return TestResult.fail testName expectedString outputString
- else
- return TestResult.error testName s!"Expected file not found: {expected}"
-
- catch e =>
- return TestResult.error testName (toString e)
-
-/-- Find all .input files in the test directory -/
-def findInputFiles (testDir : System.FilePath) : IO (Array String) := do
- let entries ← testDir.readDir
- return entries.filterMap fun f =>
- f.fileName.dropSuffix? ".input" <&> (·.toString)
-
-
-/-- Print test result -/
-def TestResult.print (result : TestResult) : IO Unit := do
- match result with
- | .pass name =>
- IO.println s!"✓ {name}"
- | .fail name expected actual =>
- IO.println s!"✗ {name}"
- IO.println s!" Expected output differs from actual output"
- let d := diff (expected.splitToList (· == '\n') |>.toArray) (actual.splitToList (· == '\n') |>.toArray)
- IO.println (linesToString d)
- | .error name msg =>
- IO.println s!"✗ {name}"
- IO.println s!" Error: {msg}"
-
-/-- Print final statistics -/
-def printStats (stats : TestStats) : IO Unit := do
- let total := stats.total
- IO.println ""
- IO.println s!"Tests run: {total}"
- IO.println s!"Passed: {stats.passed}"
- if stats.failed > 0 then
- IO.println s!"Failed: {stats.failed}"
- if stats.errors > 0 then
- IO.println s!"Errors: {stats.errors}"
-
- if stats.failed == 0 && stats.errors == 0 then
- IO.println "All tests passed! ✓"
- else
- IO.println s!"Some tests failed. ✗"
-
-/-- Main test runner -/
-def runTests (config : Config) : IO Unit := do
- unless ← System.FilePath.pathExists config.testDir do
- throw <| .userError s!"Test directory not found: {config.testDir}"
-
- let inputFiles ← findInputFiles config.testDir
-
- if inputFiles.isEmpty then
- IO.println s!"No .input files found in {config.testDir}"
- return
-
- if config.updateExpected then
- IO.println s!"Updating expected outputs in {config.testDir}..."
- else
- IO.println s!"Running tests in {config.testDir}..."
- IO.println ""
-
- let mut stats : TestStats := {}
- for inputFile in inputFiles do
- let result ← runSingleTest config inputFile
- result.print
- stats := stats.add result
-
- printStats stats
-
- if stats.failed == 0 && stats.errors == 0 then return
- else throw <| .userError s!"Failed with {stats.failed} failures and {stats.errors} errors"
diff --git a/src/tests/Tests/Integration.lean b/src/tests/Tests/Integration.lean
deleted file mode 100644
index eae1daa30..000000000
--- a/src/tests/Tests/Integration.lean
+++ /dev/null
@@ -1,127 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: Jason Reed
--/
-module
-
-public import Lean.Util.Diff
-
-public section
-
-namespace Verso.Integration
-
-/-- Configuration for the test runner -/
-structure Config where
- /-- Where are expected files located? We expect a subdirectory
- `expected` and `runTest` should produce files into a subdirectory
- `output`. -/
- testDir : System.FilePath
- /-- Should the expected output be replaced with the actual output? -/
- updateExpected : Bool := false
- /-- How to run the test -/
- runTest : IO Unit
- /-- Whether to see if lualatex builds the file -/
- checkTeX : Bool
-
-/--
-Returns all non-directory filepaths that are children of `root`, which
-must be a directory. Returns these as paths relative to `root`.
-
-This differs from `System.FilePath.walkRoot`, in that the latter returns
-absolute paths, and includes subdirectories.
--/
-partial def filesBelow (root : System.FilePath) :
- IO (Array System.FilePath) := do
- let files ← Prod.snd <$> StateT.run (go ".") #[]
- return files.qsort (·.toString < ·.toString) -- Ensure deterministic result
-where
- go (p : System.FilePath) := do
- for d in (← (root / p).readDir) do
- if ← d.path.isDir then
- go (p / d.fileName)
- else
- modify (·.push (p / d.fileName))
-
-/--
-Given an array of pairs `(src, tgt)` of absolute paths, copy every
-`src` to every `tgt`, creating directories as necessary.
--/
-partial def copyFiles (pairs : Array (System.FilePath × System.FilePath)) :
- IO Unit := do
- for (src, tgt) in pairs do
- if let .some parent := tgt.parent then
- IO.FS.createDirAll parent
- IO.FS.writeBinFile tgt (← IO.FS.readBinFile src)
-
-/-- Main test runner -/
-def runTests (config : Config) : IO Unit := do
- let outputRoot := config.testDir / "output"
- let expectedRoot := config.testDir / "expected"
-
- if config.updateExpected then
- -- Create the test directory if it doesn't exist
- unless ← System.FilePath.pathExists config.testDir do
- IO.FS.createDirAll config.testDir
- unless ← System.FilePath.pathExists outputRoot do
- IO.FS.createDirAll outputRoot
- config.runTest
- let outputFiles := (← filesBelow outputRoot)
- IO.println s!"Updating expected outputs in {config.testDir}..."
- if ← System.FilePath.pathExists expectedRoot then do
- IO.FS.removeDirAll expectedRoot
- copyFiles (outputFiles.map (fun p => (outputRoot / p, expectedRoot / p)))
- else
- unless ← System.FilePath.pathExists config.testDir do
- throw <| .userError s!"Test directory not found: {config.testDir}"
- unless ← System.FilePath.pathExists expectedRoot do
- IO.FS.createDirAll expectedRoot
- let expectedFiles := (← filesBelow expectedRoot)
-
- IO.println s!"Running test in {config.testDir}..."
- if ← outputRoot.pathExists then
- IO.FS.removeDirAll outputRoot
- config.runTest
- let outputFiles := (← filesBelow outputRoot)
-
- if expectedFiles != outputFiles then
- IO.println s!"✗ Expected files differ from actual files"
- IO.println s!"Expected files in {expectedRoot}:\n {expectedFiles}"
- IO.println s!"Actual files in {outputRoot}:\n {outputFiles}"
- throw <| .userError s!"Test in {config.testDir} failed"
-
- for file in expectedFiles do
- let expected ← IO.FS.readFile (expectedRoot / file)
- let actual ← IO.FS.readFile (outputRoot / file)
- if expected != actual then
- let d := Lean.Diff.diff (expected.splitToList (· == '\n') |>.toArray) (actual.splitToList (· == '\n') |>.toArray)
- IO.println s!"✗ In test {config.testDir}, output file {file}"
- IO.println s!" Expected output differs from actual output"
- IO.println (Lean.Diff.linesToString d)
- throw <| .userError s!"Test in {config.testDir} failed"
-
- if config.checkTeX then
- -- `-shell-escape` is required so that documents using the `svg` LaTeX package can call
- -- Inkscape to rasterise SVG attachments emitted by `diagram` code blocks.
- let texDir := outputRoot / "tex"
- let result ← IO.Process.output {
- cwd := texDir,
- cmd := "lualatex",
- args := #["-shell-escape", "-halt-on-error", "-interaction=nonstopmode", "main.tex"]
- }
- if result.exitCode != 0 then
- -- lualatex writes its diagnostics to stdout and `main.log`, not stderr, so report all
- -- three. Each is wrapped in a GitHub Actions log group so the output stays collapsible.
- let group (title : String) (body : String) : IO Unit := do
- IO.println s!"::group::{title}"
- IO.println body
- IO.println "::endgroup::"
- let context := s!"lualatex in {config.testDir}"
- group s!"{context}: stdout" result.stdout
- group s!"{context}: stderr" result.stderr
- let logFile := texDir / "main.log"
- if ← logFile.pathExists then
- group s!"{context}: {logFile}" (← IO.FS.readFile logFile)
- throw <| .userError s!"lualatex exited with code {result.exitCode} in {config.testDir}"
-
- return
diff --git a/src/tests/Tests/LiterateConfig.lean b/src/tests/Tests/LiterateConfig.lean
deleted file mode 100644
index 8327d3966..000000000
--- a/src/tests/Tests/LiterateConfig.lean
+++ /dev/null
@@ -1,362 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-module
-public import VersoLiterate
-
-public section
-
-open Lean
-open VersoLiterate
-
-namespace Tests.LiterateConfig
-
-/-- Parses a TOML string directly into a `LiterateConfig`. -/
-private def loadFromString (toml : String) : IO LiterateConfig :=
- parseLiterateConfig toml
-
-/-- Asserts that `actual` equals `expected`, throwing with a descriptive message on failure. -/
-private def assertEq [BEq α] [Repr α] (desc : String) (actual expected : α) : IO Unit :=
- unless actual == expected do
- throw <| IO.userError s!"{desc}: expected {repr expected}, got {repr actual}"
-
-private def assertTrue (desc : String) (b : Bool) : IO Unit :=
- unless b do
- throw <| IO.userError s!"{desc}: expected true, got false"
-
-private def assertFalse (desc : String) (b : Bool) : IO Unit := do
- if b then
- throw <| IO.userError s!"{desc}: expected false, got true"
-
-private def assertSome [Repr α] (desc : String) (o : Option α) : IO α :=
- match o with
- | some v => pure v
- | none => throw <| IO.userError s!"{desc}: expected Some, got None"
-
-private def assertNone [Repr α] (desc : String) (o : Option α) : IO Unit := do
- if o.isSome then
- throw <| IO.userError s!"{desc}: expected None, got {repr o}"
-
--- ===== Individual test cases =====
-
-/-- A missing file results in the default config. -/
-private def testMissingFile : IO Unit := do
- let config ← loadLiterateConfig "/nonexistent/path/literate.toml"
- assertEq "missing file: exclude" config.exclude #[]
- assertEq "missing file: order" config.order #[]
- assertEq "missing file: targets" config.targets #[]
- assertNone "missing file: landingPage" config.landingPage
-
-/-- An empty file results in the default config. -/
-private def testEmptyFile : IO Unit := do
- let config ← loadFromString ""
- assertEq "empty file: exclude" config.exclude #[]
- assertEq "empty file: order" config.order #[]
- assertEq "empty file: targets" config.targets #[]
- assertNone "empty file: landingPage" config.landingPage
-
-/-- A whitespace-only file results in the default config. -/
-private def testWhitespaceFile : IO Unit := do
- let config ← loadFromString " \n \n "
- assertEq "whitespace file: exclude" config.exclude #[]
-
-/-- The `exclude` list is parsed into an array of `Name` values. -/
-private def testExclude : IO Unit := do
- let config ← loadFromString "exclude = [\"Foo.Bar\", \"Baz\"]\n"
- assertEq "exclude length" config.exclude.size 2
- assertEq "exclude[0]" config.exclude[0]! `Foo.Bar
- assertEq "exclude[1]" config.exclude[1]! `Baz
-
-/-- The `order` list is parsed into an array of `Name` values preserving order. -/
-private def testOrder : IO Unit := do
- let config ← loadFromString "order = [\"C\", \"A\", \"B\"]\n"
- assertEq "order length" config.order.size 3
- assertEq "order[0]" config.order[0]! `C
- assertEq "order[1]" config.order[1]! `A
- assertEq "order[2]" config.order[2]! `B
-
-/-- The `landing_page` field is parsed as `some` of a `Name`. -/
-private def testLandingPage : IO Unit := do
- let config ← loadFromString "landing_page = \"MyLib.Overview\"\n"
- let lp ← assertSome "landing_page" config.landingPage
- assertEq "landing_page value" lp `MyLib.Overview
-
-/-- `[order_children]` entries are parsed into per-parent child orderings. -/
-private def testOrderChildren : IO Unit := do
- let config ← loadFromString "[order_children]\n\"Foo\" = [\"Foo.B\", \"Foo.A\"]\n\"Bar\" = [\"Bar.Z\"]\n"
- let fooChildren := config.orderChildren.find? `Foo
- let fc ← assertSome "order_children: Foo" fooChildren
- assertEq "order_children: Foo length" fc.size 2
- assertEq "order_children: Foo[0]" fc[0]! `Foo.B
- assertEq "order_children: Foo[1]" fc[1]! `Foo.A
- let barChildren := config.orderChildren.find? `Bar
- let bc ← assertSome "order_children: Bar" barChildren
- assertEq "order_children: Bar length" bc.size 1
- assertEq "order_children: Bar[0]" bc[0]! `Bar.Z
-
-/-- `[[targets]]` table entries are parsed into `Target` values with optional fields. -/
-private def testTargets : IO Unit := do
- let config ← loadFromString "[[targets]]\nmodule = \"Foo\"\n\n[[targets]]\nlibrary = \"Bar\"\n"
- assertEq "targets length" config.targets.size 2
- let t0 ← assertSome "targets[0].module" config.targets[0]!.module
- assertEq "targets[0].module value" t0 `Foo
- assertNone "targets[0].library" config.targets[0]!.library
- let t1 ← assertSome "targets[1].library" config.targets[1]!.library
- assertEq "targets[1].library value" t1 `Bar
- assertNone "targets[1].module" config.targets[1]!.module
-
-/-- Multiple fields in the same file are all parsed correctly. -/
-private def testCombined : IO Unit := do
- let toml := "exclude = [\"Private\"]\norder = [\"Public\", \"Examples\"]\nlanding_page = \"Public\"\n"
- let config ← loadFromString toml
- assertEq "combined: exclude length" config.exclude.size 1
- assertEq "combined: exclude[0]" config.exclude[0]! `Private
- assertEq "combined: order length" config.order.size 2
- assertEq "combined: order[0]" config.order[0]! `Public
- assertEq "combined: order[1]" config.order[1]! `Examples
- let lp ← assertSome "combined: landing_page" config.landingPage
- assertEq "combined: landing_page value" lp `Public
-
-/-- Invalid TOML produces an error. -/
-private def testInvalidToml : IO Unit := do
- let mut caught := false
- try
- let _ ← loadFromString "this is not valid toml {{{"
- catch _ =>
- caught := true
- assertTrue "invalid TOML should throw" caught
-
-/-- `hide_commands` is parsed into an array of keyword pattern strings. -/
-private def testHideCommands : IO Unit := do
- let config ← loadFromString "hide_commands = [\"set_option\", \"#check\"]\n"
- assertEq "hide_commands length" config.hideCommands.size 2
- assertEq "hide_commands[0]" config.hideCommands[0]! "set_option"
- assertEq "hide_commands[1]" config.hideCommands[1]! "#check"
-
-/-- `[metadata]` table is parsed into a `Metadata` value. -/
-private def testMetadata : IO Unit := do
- let config ← loadFromString "[metadata]\ntitle = \"My Site\"\ndescription = \"A test site\"\nfavicon = \"favicon.ico\"\n"
- let title ← assertSome "metadata.title" config.metadata.title
- assertEq "metadata.title value" title "My Site"
- let desc ← assertSome "metadata.description" config.metadata.description
- assertEq "metadata.description value" desc "A test site"
- let fav ← assertSome "metadata.favicon" config.metadata.favicon
- assertEq "metadata.favicon value" fav "favicon.ico"
-
-/-- `extra_css` and `extra_js` are parsed into string arrays. -/
-private def testExtraCssJs : IO Unit := do
- let config ← loadFromString "extra_css = [\"custom.css\", \"theme.css\"]\nextra_js = [\"analytics.js\"]\n"
- assertEq "extra_css length" config.extraCss.size 2
- assertEq "extra_css[0]" config.extraCss[0]! "custom.css"
- assertEq "extra_css[1]" config.extraCss[1]! "theme.css"
- assertEq "extra_js length" config.extraJs.size 1
- assertEq "extra_js[0]" config.extraJs[0]! "analytics.js"
-
-/-- `show_docstrings = false` is parsed correctly. -/
-private def testShowDocstrings : IO Unit := do
- let config ← loadFromString "show_docstrings = false\n"
- assertFalse "show_docstrings" config.showDocstrings
-
-/-- `show_docstrings_for` is parsed into an array of `Name` values. -/
-private def testShowDocstringsFor : IO Unit := do
- let config ← loadFromString "show_docstrings = false\nshow_docstrings_for = [\"Foo.bar\", \"Baz.qux\"]\n"
- assertFalse "show_docstrings" config.showDocstrings
- assertEq "show_docstrings_for length" config.showDocstringsFor.size 2
- assertEq "show_docstrings_for[0]" config.showDocstringsFor[0]! `Foo.bar
- assertEq "show_docstrings_for[1]" config.showDocstringsFor[1]! `Baz.qux
-
-/-- `hide_docstrings_for` is parsed into an array of `Name` values. -/
-private def testHideDocstringsFor : IO Unit := do
- let config ← loadFromString "hide_docstrings_for = [\"Foo.internal\"]\n"
- assertTrue "show_docstrings default" config.showDocstrings
- assertEq "hide_docstrings_for length" config.hideDocstringsFor.size 1
- assertEq "hide_docstrings_for[0]" config.hideDocstringsFor[0]! `Foo.internal
-
-/-- `show_output` is parsed into an array of keyword pattern strings. -/
-private def testShowOutput : IO Unit := do
- let config ← loadFromString "show_output = [\"#eval\"]\n"
- assertEq "show_output length" config.showOutput.size 1
- assertEq "show_output[0]" config.showOutput[0]! "#eval"
-
-/-- `show_output` defaults to the standard 4-element list. -/
-private def testShowOutputDefault : IO Unit := do
- let config ← loadFromString ""
- assertEq "show_output default length" config.showOutput.size 4
-
-/-- `show_imports = false` is parsed correctly. -/
-private def testShowImports : IO Unit := do
- let config ← loadFromString "show_imports = false\n"
- assertFalse "show_imports" config.showImports
-
-/-- `show_imports` defaults to true. -/
-private def testShowImportsDefault : IO Unit := do
- let config ← loadFromString ""
- assertTrue "show_imports default" config.showImports
-
-/-- Multiple new fields combined in one config. -/
-private def testCombinedNew : IO Unit := do
- let toml := String.intercalate "\n" [
- "exclude = [\"Private\"]",
- "hide_commands = [\"set_option\"]",
- "extra_css = [\"style.css\"]",
- "show_docstrings = false",
- "show_docstrings_for = [\"Public.api\"]",
- "[metadata]",
- "title = \"Test\"",
- ""
- ]
- let config ← loadFromString toml
- assertEq "combined new: exclude" config.exclude.size 1
- assertEq "combined new: hide_commands" config.hideCommands.size 1
- assertEq "combined new: extra_css" config.extraCss.size 1
- assertFalse "combined new: show_docstrings" config.showDocstrings
- assertEq "combined new: show_docstrings_for" config.showDocstringsFor.size 1
- let title ← assertSome "combined new: metadata.title" config.metadata.title
- assertEq "combined new: metadata.title value" title "Test"
-
-/-- `[theme]` light variables are parsed into theme map. -/
-private def testThemeLight : IO Unit := do
- let toml := "[theme]\ncode_box_background_color = \"#fff\"\ntext_color = \"#111\"\n"
- let config ← loadFromString toml
- assertEq "theme size" config.theme.size 2
- let bg ← assertSome "theme code_box_background_color" (config.theme.get? "code_box_background_color")
- assertEq "theme code_box_background_color value" bg "#fff"
- let tc ← assertSome "theme text_color" (config.theme.get? "text_color")
- assertEq "theme text_color value" tc "#111"
-
-/-- `[theme.dark]` dark variables are parsed into themeDark map. -/
-private def testThemeDark : IO Unit := do
- let toml := "[theme]\ntext_color = \"#333\"\n\n[theme.dark]\ntext_color = \"#eee\"\nbackground_color = \"#111\"\n"
- let config ← loadFromString toml
- assertEq "theme light size" config.theme.size 1
- assertEq "themeDark size" config.themeDark.size 2
- let dt ← assertSome "themeDark text_color" (config.themeDark.get? "text_color")
- assertEq "themeDark text_color value" dt "#eee"
- let db ← assertSome "themeDark background_color" (config.themeDark.get? "background_color")
- assertEq "themeDark background_color value" db "#111"
-
-/-- Empty theme produces empty maps. -/
-private def testThemeEmpty : IO Unit := do
- let config ← loadFromString ""
- assertEq "theme empty size" config.theme.size 0
- assertEq "themeDark empty size" config.themeDark.size 0
-
-/-- `[modules."Foo.Bar"]` is parsed into a ModuleConfig. -/
-private def testModulesConfig : IO Unit := do
- let toml := "[modules.\"Foo.Bar\"]\ntitle = \"Custom Title\"\nurl = \"custom-url\"\nhide_commands = [\"set_option\"]\nshow_imports = false\n"
- let config ← loadFromString toml
- let mc ← assertSome "modules Foo.Bar" (config.modules.find? `Foo.Bar)
- let t ← assertSome "modules Foo.Bar title" mc.title
- assertEq "modules Foo.Bar title value" t "Custom Title"
- let u ← assertSome "modules Foo.Bar url" mc.url
- assertEq "modules Foo.Bar url value" u "custom-url"
- let hc ← assertSome "modules Foo.Bar hideCommands" mc.hideCommands
- assertEq "modules Foo.Bar hideCommands length" hc.size 1
- let si ← assertSome "modules Foo.Bar showImports" mc.showImports
- assertFalse "modules Foo.Bar showImports value" si
-
-/-- `resolveForModule` returns global defaults when no module config matches. -/
-private def testResolveNoMatch : IO Unit := do
- let config ← loadFromString "hide_commands = [\"set_option\"]\n"
- let resolved := config.resolveForModule `Unmatched.Module
- assertEq "resolve no match: hideCommands" resolved.hideCommands.size 1
- assertTrue "resolve no match: showImports" resolved.showImports
- assertNone "resolve no match: title" resolved.title
-
-/-- `resolveForModule` picks the most-specific prefix match. -/
-private def testResolvePrefixMatch : IO Unit := do
- let toml := String.intercalate "\n" [
- "hide_commands = [\"set_option\"]",
- "[modules.\"Foo\"]",
- "show_imports = false",
- "[modules.\"Foo.Bar\"]",
- "title = \"Bar Title\"",
- "show_imports = true",
- ""
- ]
- let config ← loadFromString toml
- -- Foo.Bar.Baz should match Foo.Bar (longest prefix)
- let resolved := config.resolveForModule `Foo.Bar.Baz
- let t ← assertSome "resolve prefix: title" resolved.title
- assertEq "resolve prefix: title value" t "Bar Title"
- assertTrue "resolve prefix: showImports" resolved.showImports
- -- Foo.Qux should match Foo
- let resolved2 := config.resolveForModule `Foo.Qux
- assertFalse "resolve Foo prefix: showImports" resolved2.showImports
- assertNone "resolve Foo prefix: title" resolved2.title
- -- Exact match on Foo.Bar itself
- let resolved3 := config.resolveForModule `Foo.Bar
- let t3 ← assertSome "resolve exact: title" resolved3.title
- assertEq "resolve exact: title value" t3 "Bar Title"
-
-/-- Module-level config overrides global defaults. -/
-private def testResolveOverridesGlobal : IO Unit := do
- let toml := String.intercalate "\n" [
- "show_imports = true",
- "show_docstrings = true",
- "[modules.\"MyMod\"]",
- "show_imports = false",
- "show_docstrings = false",
- ""
- ]
- let config ← loadFromString toml
- let resolved := config.resolveForModule `MyMod
- assertFalse "resolve override: showImports" resolved.showImports
- assertFalse "resolve override: showDocstrings" resolved.showDocstrings
- -- Unmatched module still gets global defaults
- let resolved2 := config.resolveForModule `Other
- assertTrue "resolve global: showImports" resolved2.showImports
- assertTrue "resolve global: showDocstrings" resolved2.showDocstrings
-
--- ===== Test runner =====
-
-private def configTests : List (String × IO Unit) := [
- ("missing file", testMissingFile),
- ("empty file", testEmptyFile),
- ("whitespace file", testWhitespaceFile),
- ("exclude", testExclude),
- ("order", testOrder),
- ("landing_page", testLandingPage),
- ("order_children", testOrderChildren),
- ("targets", testTargets),
- ("combined", testCombined),
- ("invalid TOML", testInvalidToml),
- ("hide_commands", testHideCommands),
- ("metadata", testMetadata),
- ("extra_css/js", testExtraCssJs),
- ("show_docstrings", testShowDocstrings),
- ("show_docstrings_for", testShowDocstringsFor),
- ("hide_docstrings_for", testHideDocstringsFor),
- ("show_output", testShowOutput),
- ("show_output default", testShowOutputDefault),
- ("show_imports", testShowImports),
- ("show_imports default", testShowImportsDefault),
- ("combined new", testCombinedNew),
- ("theme light", testThemeLight),
- ("theme dark", testThemeDark),
- ("theme empty", testThemeEmpty),
- ("modules config", testModulesConfig),
- ("resolve no match", testResolveNoMatch),
- ("resolve prefix match", testResolvePrefixMatch),
- ("resolve overrides global", testResolveOverridesGlobal)
-]
-
-def runLiterateConfigTests : IO Nat := do
- IO.println "Running literate config unit tests..."
- let mut failures := 0
- for (name, test) in configTests do
- try
- test
- IO.println s!" {name}: passed"
- catch e =>
- IO.eprintln s!" {name}: FAILED - {e}"
- failures := failures + 1
- if failures == 0 then
- IO.println " All literate config tests passed."
- else
- IO.eprintln s!" {failures} literate config test(s) failed."
- return failures
-
-end Tests.LiterateConfig
diff --git a/src/tests/Tests/SearchJs.lean b/src/tests/Tests/SearchJs.lean
deleted file mode 100644
index 88008dc52..000000000
--- a/src/tests/Tests/SearchJs.lean
+++ /dev/null
@@ -1,117 +0,0 @@
-/-
-Copyright (c) 2026 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-module
-public import VersoSearch
-public import VersoSearch.DomainSearch
-
-/-!
-Tests for the JavaScript wire format produced by `Verso.Search.DomainMapper.toJs` and
-`Verso.Search.DomainMappers.toJs`. These are structural checks against the emitted JS source: they
-assert that priority fields and global priority exports appear with the configured values, so the
-browser-side combining code in `search-box.js` has the data it expects.
--/
-
-namespace Verso.Tests.SearchJs
-
-open Std
-open Verso.Search
-
-private def hasSub (haystack : String) (needle : String) : Bool :=
- haystack.find? needle |>.isSome
-
-private def assertContains (label : String) (haystack : String) (needle : String) : IO Unit := do
- unless hasSub haystack needle do
- throw <| IO.userError s!"expected {label} output to contain {repr needle}, got:\n{haystack}"
-
-/-- Verifies that `DomainMapper.toJs` emits the display/class/data fields without a priority. -/
-def testMapperToJs : IO Unit := do
- let mapper : DomainMapper :=
- { displayName := "Term"
- className := "term"
- dataToSearchables := "x => []" }
- let rendered := (DomainMapper.toJs mapper).pretty (width := 70)
- assertContains "DomainMapper" rendered "displayName:"
- assertContains "DomainMapper" rendered "\"Term\""
- assertContains "DomainMapper" rendered "className:"
- assertContains "DomainMapper" rendered "\"term\""
- assertContains "DomainMapper" rendered "dataToSearchables:"
- if hasSub rendered "searchPriority" then
- throw <| IO.userError
- s!"DomainMapper output should not contain `searchPriority` (it lives in SearchPriorities now):\n{rendered}"
-
-/--
-Verifies that `DomainMappers.toJs` emits both the `domainMappers` constant and the
-`searchPriorities` constant with the correct semantic / fullText values plus the per-domain
-priorities map.
--/
-def testMappersToJs : IO Unit := do
- let mapper : DomainMapper :=
- { displayName := "Term"
- className := "term"
- dataToSearchables := "x => []" }
- let mappers : DomainMappers := HashMap.ofList [("Verso.Test", mapper)]
- let priorities : SearchPriorities :=
- { semantic := 60, fullText := 40, domains := ({} : Verso.NameMap _).insert `Verso.Test 73 }
- let rendered := (mappers.toJs priorities).pretty (width := 70)
- assertContains "DomainMappers" rendered "export const domainMappers"
- assertContains "DomainMappers" rendered "export const searchPriorities"
- assertContains "DomainMappers" rendered "semantic:"
- assertContains "DomainMappers" rendered "60"
- assertContains "DomainMappers" rendered "fullText:"
- assertContains "DomainMappers" rendered "40"
- assertContains "DomainMappers" rendered "domains:"
- assertContains "DomainMappers" rendered "\"Verso.Test\""
- assertContains "DomainMappers" rendered "73"
-
-/--
-Verifies that `Verso.Search.priorityMapJson` produces a keyed map of only the documents whose priority
-differs from neutral, using the same centered-at-50 integer convention as `Searchable.priority`.
--/
-def testPriorityMap : IO Unit := do
- let docs : Array IndexDoc := #[
- { id := "boosted", header := "", context := #[], content := "", priority := some 80 },
- { id := "no-priority", header := "", context := #[], content := "", priority := none },
- -- A `some 50` is semantically equivalent to `none` and must not bloat the emitted map:
- { id := "explicit-neutral", header := "", context := #[], content := "", priority := some 50 },
- { id := "suppressed", header := "", context := #[], content := "", priority := some 10 },
- -- Ancestor-summed priorities can fall outside [0, 99]:
- { id := "deep-subsection", header := "", context := #[], content := "", priority := some (-20) }
- ]
- let rendered := (priorityMapJson docs).compress
- assertContains "priorityMapJson" rendered "\"boosted\":80"
- assertContains "priorityMapJson" rendered "\"suppressed\":10"
- assertContains "priorityMapJson" rendered "\"deep-subsection\":-20"
- -- Neutral docs (none or some 50) must be omitted entirely, not serialized as null or 50.
- for omitted in ["no-priority", "explicit-neutral"] do
- if hasSub rendered omitted then
- throw <| IO.userError
- s!"priorityMapJson should omit neutral docs ({omitted}), but emitted:\n{rendered}"
-
-/-- Defaults for `SearchPriorities` are `semantic := 50` and `fullText := 50`. -/
-def testMappersToJsDefaults : IO Unit := do
- let mappers : DomainMappers := {}
- let rendered := (mappers.toJs).pretty (width := 70)
- assertContains "DomainMappers defaults" rendered "export const searchPriorities"
- assertContains "DomainMappers defaults" rendered "semantic:"
- assertContains "DomainMappers defaults" rendered "fullText:"
- assertContains "DomainMappers defaults" rendered "50"
-
-public def runSearchJsTests : IO Nat := do
- let tests : List (Lean.Name × IO Unit) :=
- [ (`testMapperToJs, testMapperToJs)
- , (`testMappersToJs, testMappersToJs)
- , (`testMappersToJsDefaults, testMappersToJsDefaults)
- , (`testPriorityMap, testPriorityMap)
- ]
- let mut failures := 0
- for (name, test) in tests do
- try
- test
- IO.println s!"{name}: passed"
- catch e =>
- IO.println s!"{name}: FAILED - {e}"
- failures := failures + 1
- return failures
diff --git a/src/tests/Tests/VersoManual.lean b/src/tests/Tests/VersoManual.lean
deleted file mode 100644
index 4305cd6c1..000000000
--- a/src/tests/Tests/VersoManual.lean
+++ /dev/null
@@ -1,12 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-import Tests.VersoManual.Docstring
-import Tests.VersoManual.Html
-import Tests.VersoManual.Html.SoftHyphenate
-import Tests.VersoManual.License
-import Tests.VersoManual.Markdown
-import Tests.VersoManual.Sections
-import Tests.VersoManual.WordCount
diff --git a/src/tests/Tests/Zip.lean b/src/tests/Tests/Zip.lean
deleted file mode 100644
index f63aad0b2..000000000
--- a/src/tests/Tests/Zip.lean
+++ /dev/null
@@ -1,32 +0,0 @@
-/-
-Copyright (c) 2025 Lean FRO LLC. All rights reserved.
-Released under Apache 2.0 license as described in the file LICENSE.
-Author: David Thrane Christiansen
--/
-module
-
-public import VersoUtil.Zip
-
-public section
-
-open Verso.Zip
-
-def testExtract (files : Array (String × ByteArray)) (method : CompressionMethod) : IO Unit := do
- IO.FS.withTempDir fun dir => do
- let extra ← IO.monoMsNow
- let dir := dir / s!"{extra}"
- IO.FS.createDirAll dir
- let file := dir / "out.zip"
-
- zipToFile file files method
- let out ← IO.Process.output {cmd := "unzip", args := #["-u", file.toString, "-d", dir.toString]}
- -- unzip returns 1 on empty archives, 2 on corrupt archives
- if out.exitCode == 0 || (files.isEmpty && out.exitCode == 1) then
- for (f, contents) in files do
- let found ← IO.FS.readBinFile (dir / f)
- if found != contents then
- throw <| .userError s!"Mismatched file contents of {f}. Expected {contents}, got {found}"
- else
- throw <| IO.userError s!"process 'unzip' exited with code {out.exitCode}\
- \nstderr:\
- \n{out.stderr}"
diff --git a/src/tests/VersoTests.lean b/src/tests/VersoTests.lean
new file mode 100644
index 000000000..487403062
--- /dev/null
+++ b/src/tests/VersoTests.lean
@@ -0,0 +1,11 @@
+/-
+Copyright (c) 2026 Lean FRO LLC. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Author: David Thrane Christiansen
+-/
+module
+
+/-!
+Errata ports of the Verso test suite. Each feature lives in its own submodule, discovered by the
+Errata test driver.
+-/
diff --git a/src/tests/Tests/Arbitrary.lean b/src/tests/VersoTests/Arbitrary.lean
similarity index 94%
rename from src/tests/Tests/Arbitrary.lean
rename to src/tests/VersoTests/Arbitrary.lean
index bfe69e9a8..737347f40 100644
--- a/src/tests/Tests/Arbitrary.lean
+++ b/src/tests/VersoTests/Arbitrary.lean
@@ -6,20 +6,19 @@ Author: David Thrane Christiansen
module
public import Plausible
public import Plausible.ArbitraryFueled
-public meta import Plausible.ArbitraryFueled
import Lean.Data.Json.FromToJson
import all MultiVerso.InternalId
-public meta import MultiVerso.NameMap
-public meta import MultiVerso
-public meta import VersoManual.Html.JsFile
-public meta import VersoManual.Html.CssFile
-public meta import VersoManual.Html.Features
-public meta import VersoManual.LicenseInfo
-public meta import VersoSearch
-public meta import VersoSearch.DomainSearch
-public meta import Verso.Output.Html
-public meta import MultiVerso.Manifest
-public meta import VersoManual.Basic
+public import MultiVerso.NameMap
+public import MultiVerso
+public import VersoManual.Html.JsFile
+public import VersoManual.Html.CssFile
+public import VersoManual.Html.Features
+public import VersoManual.LicenseInfo
+public import VersoSearch
+public import VersoSearch.DomainSearch
+public import Verso.Output.Html
+public import MultiVerso.Manifest
+public import VersoManual.Basic
import all VersoManual.Basic
import VersoManual.Html.CssFile
@@ -35,7 +34,7 @@ deserializes.
-/
-public meta section
+public section
def sizedArrayOf (gen : Gen α) : Gen (Array α) := do
let count ← chooseNat
@@ -251,12 +250,11 @@ instance : Shrinkable LetterString where
(instShrinkableLetter.shrink first |>.map (String.singleton · ++ rest))
else []
-def slugChars := Slug.validChars.toArray
+def slugChars : Array Char := Slug.validChars.toArray
def slugChar : Gen Char := do
- have : slugChars.size > 0 := by decide +native
- let ⟨i, ⟨_, h⟩⟩ ← choose Nat 0 (slugChars.size - 1) (by simp)
- return slugChars[i]'(by grind)
+ let ⟨i, _⟩ ← choose Nat 0 (slugChars.size - 1) (by omega)
+ return slugChars[i]!
def slugString : Gen String := do
let len ← chooseNat
diff --git a/src/tests/Tests/Basic.lean b/src/tests/VersoTests/Basic.lean
similarity index 96%
rename from src/tests/Tests/Basic.lean
rename to src/tests/VersoTests/Basic.lean
index f0e1dd932..cb27067eb 100644
--- a/src/tests/Tests/Basic.lean
+++ b/src/tests/VersoTests/Basic.lean
@@ -3,10 +3,8 @@ Copyright (c) 2023 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
-module
-public import Verso
-public meta import Verso
-public section
+import Errata
+import Verso
namespace Verso.BasicTest
set_option guard_msgs.diff true
set_option pp.rawOnError true
@@ -18,7 +16,7 @@ set_option pp.rawOnError true
:::::::
:::::::
/-- info: Verso.Doc.Part.mk #[Verso.Doc.Inline.text "Nothing"] "Nothing" none #[] #[] -/
-#guard_msgs in
+#test_msgs in
#eval noDoc.toPart
@@ -38,7 +36,7 @@ info: Verso.Doc.Part.mk
#[Verso.Doc.Block.para #[Verso.Doc.Inline.text "Hello, I'm a paragraph. Yes I am!"]]
#[]
-/
-#guard_msgs in
+#test_msgs in
#eval littleParagraph.toPart
@@ -58,7 +56,7 @@ info: Verso.Doc.Part.mk
#[Verso.Doc.Block.ul #[{ contents := #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "Just a list with one item"]] }]]
#[]
-/
-#guard_msgs in
+#test_msgs in
#eval listOneItem.toPart
@@ -85,7 +83,7 @@ info: Verso.Doc.Part.mk
#[Verso.Doc.Block.para #[Verso.Doc.Inline.text "a paragraph"]]
#[]]
-/
-#guard_msgs in
+#test_msgs in
#eval sectionAndPara.toPart
@@ -130,7 +128,7 @@ info: Verso.Doc.Part.mk
#[{ contents := #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "and nested"]] }]] }]]
#[]]]
-/
-#guard_msgs in
+#test_msgs in
#eval nestedDoc1.toPart
@@ -175,7 +173,7 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.ul #[{ contents := #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "and nested"]] }]]
#[]]]
-/
-#guard_msgs in
+#test_msgs in
#eval nestedDoc2.toPart
@@ -225,7 +223,7 @@ info: Verso.Doc.Part.mk
#[{ contents := #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "and nested"]] }]] }]]
#[]]]
-/
-#guard_msgs in
+#test_msgs in
#eval nestedDoc3.toPart
@@ -262,7 +260,7 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.para #[Verso.Doc.Inline.text "Also, 2 > 3."]]
#[]]
-/
-#guard_msgs in
+#test_msgs in
#eval nestedDoc4.toPart
@@ -270,7 +268,7 @@ info: Verso.Doc.Part.mk
-- https://github.com/leanprover/verso/pull/541
/-- error: Wrong header nesting - got #### but expected at most ### -/
-#guard_msgs in
+#test_msgs in
#docs (.none) h "Bad nesting" :=
:::::::
diff --git a/src/tests/Tests/VersoBlog.lean b/src/tests/VersoTests/Blog.lean
similarity index 54%
rename from src/tests/Tests/VersoBlog.lean
rename to src/tests/VersoTests/Blog.lean
index c29e1a020..6e320247d 100644
--- a/src/tests/Tests/VersoBlog.lean
+++ b/src/tests/VersoTests/Blog.lean
@@ -2,32 +2,21 @@
Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
+
+Property tests for blog identifier generation. This is a non-`module` file because `VersoBlog`
+itself is not part of the module system; the Errata runner imports it through its non-module main.
-/
-module
-public import Plausible
-public import Plausible.ArbitraryFueled
-public import VersoBlog
-public meta import VersoBlog.Basic
-public import VersoBlog.LiterateLeanPage
-public import Tests.VersoBlog.LiterateLeanPage
-public import Tests.Arbitrary
-public meta import Tests.Arbitrary
-
-public section
+import VersoBlog
+import VersoBlog.LiterateLeanPage
+import VersoTests.Arbitrary
+import Errata
open Lean
open Verso Genre Blog
open Verso.Multi
open Verso.NameMap
open Plausible Gen Arbitrary
-
-/-! ## Tests for NameSuffixMap -/
-
-/-- info: #[(`a.b.c, 1), (`a.c, 4), (`b.c, 6), (`c, 3)] -/
-#guard_msgs in
-#eval NameSuffixMap.empty |>.insert `a.b.c 1 |>.insert `b.c 2 |>.insert `c 3 |>.insert `a.c 4 |>.insert `a.b 5 |>.insert `b.c 6 |>.get `c
-
-meta section
+open Errata
def freshIdOk (hint : LetterString) (path : Path) (howMany : Nat) : Bool := Id.run do
let mut st : TraverseState := { remoteContent := {} }
@@ -38,44 +27,51 @@ def freshIdOk (hint : LetterString) (path : Path) (howMany : Nat) : Bool := Id.r
ids := ids.push i
ids.size == howMany && ids.all (ids.count · == 1)
-def freshId_first_is_hint (hint : LetterString) (path : Path) : Bool := Id.run do
+def freshIdFirstIsHint (hint : LetterString) (path : Path) : Bool := Id.run do
let st : TraverseState := { remoteContent := {} }
let i := st.freshId path hint.sluggify
hint.isEmpty || i == hint.sluggify
-def freshId_second_is_hint_with_1 (hint : LetterString) (path : Path) : Bool := Id.run do
+def freshIdSecondIsHintWith1 (hint : LetterString) (path : Path) : Bool := Id.run do
let mut st : TraverseState := { remoteContent := {} }
let i := st.freshId path hint.sluggify
st := { st with usedIds := st.usedIds.alter path (fun used? => used?.getD {} |>.insert i) }
let i' := st.freshId path hint.sluggify
i != i' && (hint.isEmpty || (i == hint.sluggify && i' == (s!"{hint}1").sluggify))
-open scoped Plausible.Decorations in
-private def testProp
- (p : Prop) (cfg : Configuration := {})
- (p' : Decorations.DecorationsOf p := by mk_decorations) [Testable p'] :
- IO (TestResult p') :=
- Testable.checkIO p' (cfg := cfg)
-
-def blogTests : List (Name × (Σ p, IO <| TestResult p)) := [
- (`freshIdOk, ⟨_, testProp <| ∀ h p n, freshIdOk h p n⟩),
- (`freshId_first_is_hint, ⟨_, testProp <| ∀ h p, freshId_first_is_hint h p⟩),
- (`freshId_second_is_hint_with_1, ⟨_, testProp <| ∀ h p, freshId_second_is_hint_with_1 h p⟩),
-]
-
-def runBlogTests : IO Nat := do
- let mut failures := 0
- for (name, test) in blogTests do
- IO.print s!"{name}: "
- let res ← test.2
- IO.println res
- unless res matches .success .. do
- failures := failures + 1
- return failures
-
-end
-
--- Regression test for hidden blog Lean blocks.
+/-- Identifiers freshly generated within a path are unique. -/
+@[test]
+def freshIdsAreUnique : Test := property (∀ h p n, freshIdOk h p n)
+
+/-- The first identifier generated for a hint is the hint itself. -/
+@[test]
+def freshIdFirst : Test := property (∀ h p, freshIdFirstIsHint h p)
+
+/-- The second identifier generated for a hint is the hint with `1` appended. -/
+@[test]
+def freshIdSecond : Test := property (∀ h p, freshIdSecondIsHintWith1 h p)
+
+/-! ## Compile-time regression tests for the blog genre -/
+
+/-- info: #[(`a.b.c, 1), (`a.c, 4), (`b.c, 6), (`c, 3)] -/
+#test_msgs in
+#eval NameSuffixMap.empty |>.insert `a.b.c 1 |>.insert `b.c 2 |>.insert `c 3 |>.insert `a.c 4 |>.insert `a.b 5 |>.insert `b.c 6 |>.get `c
+
+-- The deprecated inline Lean role warns. This standalone document asserts the warning, ahead of the
+-- streaming blog blocks below that a `#test_msgs` wrapper cannot enclose.
+/--
+warning: `{leanInline}` is deprecated; use `{lean}` instead.
+-/
+#test_msgs in
+#docs (Post) inlineLeanRoleNamesDeprecated "Inline Lean Role Names (deprecated alias)" :=
+:::::::
+```leanInit post2
+```
+
+Legacy role: {leanInline post2}`Nat.succ 1`.
+:::::::
+
+-- Hidden blog Lean blocks elaborate with their show/keep/error flags.
#doc (Post) "Hidden Lean Block Flags" =>
```leanInit post
```
@@ -96,26 +92,17 @@ example : base = 40 := rfl
#check scratch
```
--- Regression test for inline Lean role naming in Blog:
--- canonical `{lean}` works without warnings.
+-- The canonical inline Lean role works without warnings.
#docs (Post) inlineLeanRoleNames "Inline Lean Role Names" :=
```leanInit post
```
Canonical role: {lean post}`Nat.succ 1`.
-/--
-warning: `{leanInline}` is deprecated; use `{lean}` instead.
--/
-#docs (Post) inlineLeanRoleNamesDeprecated "Inline Lean Role Names (deprecated alias)" :=
-```leanInit post2
-```
-
-Legacy role: {lean post2}`Nat.succ 1`.
-
-#guard inlineLeanRoleNames.toPart.content.size > 0
-#guard inlineLeanRoleNamesDeprecated.toPart.content.size > 0
+#test_guard inlineLeanRoleNames.toPart.content.size > 0
+#test_guard inlineLeanRoleNamesDeprecated.toPart.content.size > 0
+-- The inline Lean role elaborates terms in the saved example environment.
```leanInit env
```
diff --git a/src/tests/VersoTests/BuildLog.lean b/src/tests/VersoTests/BuildLog.lean
new file mode 100644
index 000000000..ea97f5271
--- /dev/null
+++ b/src/tests/VersoTests/BuildLog.lean
@@ -0,0 +1,109 @@
+/-
+Copyright (c) 2026 Lean FRO LLC. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Author: David Thrane Christiansen
+-/
+module
+
+public import Verso
+import Errata
+
+open Verso Errata
+
+/-!
+Tests for Verso's build log: where a message's location and severity are recorded, how locations
+format, and how logging interacts with the exit code and the ambient output streams.
+-/
+
+/-- A message logged with a `pos` location is saved with that file and position. -/
+@[test]
+def savesPosition : Test := do
+ let logger ← Logger.new
+ let pos : Lean.Lsp.Position := { line := 4, character := 2 }
+ (reportError "boom" (some { file := "PosSave.lean", span := .pos pos }) : BuildLogT IO Unit).run logger
+ let errs ← logger.errors
+ let some m := errs[0]?
+ | fail s!"expected 1 saved error, got {errs.size}"
+ assertTrue (m.severity == .error) "expected error severity"
+ match m.loc with
+ | some { file := "PosSave.lean", span := .pos p } =>
+ assertEq 4 p.line
+ assertEq 2 p.character
+ | _ => fail "expected a saved `PosSave.lean` `pos` location"
+
+/-- A `range` span is likewise saved. -/
+@[test]
+def savesRange : Test := do
+ let logger ← Logger.new
+ let r : Lean.Lsp.Range :=
+ { start := { line := 1, character := 0 }, «end» := { line := 1, character := 5 } }
+ (reportWarning "careful" (some { file := "RangeSave.lean", span := .range r }) : BuildLogT IO Unit).run logger
+ let some w := (← logger.warnings)[0]?
+ | fail "expected 1 saved warning"
+ match w.loc with
+ | some { span := .range _, .. } => pure ()
+ | _ => fail "expected a `range` span to be saved"
+
+/--
+Range formatting defers to Lean's `mkErrorStringWithPos`: `file:line:col-line:col`, with a 1-based
+line and 0-based column, keeping the full end position even within one line.
+-/
+@[test]
+def rangeFormat : Test := do
+ let crossLine : LogMessage := {
+ severity := .error, text := "msg",
+ loc := some {
+ file := "CrossLine.lean",
+ span := .range { start := { line := 19, character := 4 }, «end» := { line := 20, character := 7 } }
+ }
+ }
+ assertEq "CrossLine.lean:20:4-21:7: msg" crossLine.format
+ let sameLine : LogMessage := {
+ severity := .error, text := "msg",
+ loc := some {
+ file := "SameLine.lean",
+ span := .range { start := { line := 42, character := 4 }, «end» := { line := 42, character := 21 } }
+ }
+ }
+ assertEq "SameLine.lean:43:4-43:21: msg" sameLine.format
+
+/-- A located message is formatted uniformly as `file:line:col: text` on stderr. -/
+@[test]
+def fileLocationFormat : Test := do
+ let logger ← Logger.new
+ let out ← captureOutput <|
+ (reportError "bad term" (some { file := "FileLoc.lean", span := .pos { line := 6, character := 3 } })
+ : BuildLogT IO Unit).run logger
+ let some m := (← logger.errors)[0]?
+ | fail "expected 1 saved error with a file location"
+ assertEq (some "FileLoc.lean") (m.loc.map (·.file))
+ assertContains "FileLoc.lean:7:3: bad term" out.stderr
+
+/-- Errors set a non-zero exit code; warnings do not. -/
+@[test]
+def exitCode : Test := do
+ let withErrors ← Logger.new
+ (do reportError "e1"; reportWarning "w1"; reportError "e2" : BuildLogT IO Unit).run withErrors
+ assertEq 2 (← withErrors.errors).size
+ assertEq 1 (← withErrors.warnings).size
+ assertEq 1 (← withErrors.exitCode)
+ let warningOnly ← Logger.new
+ (reportWarning "just a warning" : BuildLogT IO Unit).run warningOnly
+ assertEq 0 (← warningOnly.exitCode)
+
+/--
+Logging writes to the ambient stderr, resolved at log time: a logger created before a stderr
+redirection still writes into the redirected stream, and nothing goes to stdout.
+-/
+@[test]
+def ambientStderr : Test := do
+ let logger ← Logger.new
+ let out ← captureOutput do
+ (do
+ reportError "first problem" (some { file := "X.lean", span := .pos { line := 0, character := 0 } })
+ reportWarning "second problem" : BuildLogT IO Unit).run logger
+ assertContains "X.lean:1:0: first problem" out.stderr
+ assertContains "second problem" out.stderr
+ assertEq "" out.stdout
+ assertEq 1 (← logger.errors).size
+ assertEq 1 (← logger.warnings).size
diff --git a/src/tests/Tests/CommentSkipping.lean b/src/tests/VersoTests/CommentSkipping.lean
similarity index 86%
rename from src/tests/Tests/CommentSkipping.lean
rename to src/tests/VersoTests/CommentSkipping.lean
index e5b560b10..3b140d783 100644
--- a/src/tests/Tests/CommentSkipping.lean
+++ b/src/tests/VersoTests/CommentSkipping.lean
@@ -3,10 +3,9 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
-module
-public meta import Tests.CommentSkipping.Doc
-public meta import Tests.CommentSkipping.Doc2
-public section
+import Errata
+import VersoTests.CommentSkipping.Doc
+import VersoTests.CommentSkipping.Doc2
/-!
This test ensures that Lean's parser doesn't skip Lean comment syntax while parsing Verso blocks as
@@ -28,8 +27,8 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.para #[Verso.Doc.Inline.text "def", Verso.Doc.Inline.linebreak "\n"]]
#[]
-/
-#guard_msgs in
-#eval %doc Tests.CommentSkipping.Doc
+#test_msgs in
+#eval %doc VersoTests.CommentSkipping.Doc
/--
info: Verso.Doc.Part.mk
@@ -42,5 +41,5 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.blockquote #[(Verso.Doc.Block.para #[Verso.Doc.Inline.text "C", Verso.Doc.Inline.linebreak "\n"])]]
#[]
-/
-#guard_msgs in
-#eval %doc Tests.CommentSkipping.Doc2
+#test_msgs in
+#eval %doc VersoTests.CommentSkipping.Doc2
diff --git a/src/tests/Tests/CommentSkipping/Doc.lean b/src/tests/VersoTests/CommentSkipping/Doc.lean
similarity index 100%
rename from src/tests/Tests/CommentSkipping/Doc.lean
rename to src/tests/VersoTests/CommentSkipping/Doc.lean
diff --git a/src/tests/Tests/CommentSkipping/Doc2.lean b/src/tests/VersoTests/CommentSkipping/Doc2.lean
similarity index 100%
rename from src/tests/Tests/CommentSkipping/Doc2.lean
rename to src/tests/VersoTests/CommentSkipping/Doc2.lean
diff --git a/src/tests/Tests/DocElabExtensions/Define.lean b/src/tests/VersoTests/DocElabExtensions/Define.lean
similarity index 71%
rename from src/tests/Tests/DocElabExtensions/Define.lean
rename to src/tests/VersoTests/DocElabExtensions/Define.lean
index 689f9725c..5532c0b13 100644
--- a/src/tests/Tests/DocElabExtensions/Define.lean
+++ b/src/tests/VersoTests/DocElabExtensions/Define.lean
@@ -3,13 +3,8 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Emilio Jesus Gallego Arias
-/
-module
-public import Tests.DocElabExtensions.LocalExtension
-public meta import Tests.DocElabExtensions.LocalExtension
-public import VersoManual
-public meta import VersoManual
-
-public section
+import VersoTests.DocElabExtensions.LocalExtension
+import VersoManual
namespace Verso.Tests.DocElabExtensions
@@ -27,19 +22,19 @@ open Lean
#register_inherited_test_local_entry
@[role]
-meta def inheritedRole : RoleExpanderOf Unit
+def inheritedRole : RoleExpanderOf Unit
| (), _contents => ``(Doc.Inline.text "inherited role")
@[code_block]
-meta def inheritedCode : CodeBlockExpanderOf Unit
+def inheritedCode : CodeBlockExpanderOf Unit
| (), str => ``(Doc.Block.code $(quote str.getString))
@[directive]
-meta def inheritedDirective : DirectiveExpanderOf Unit
+def inheritedDirective : DirectiveExpanderOf Unit
| (), blocks => do
let blocks ← blocks.mapM elabBlock
``(Doc.Block.concat #[$blocks,*])
@[block_command]
-meta def inheritedCommand : BlockCommandOf Unit
+def inheritedCommand : BlockCommandOf Unit
| () => ``(Doc.Block.para #[Doc.Inline.text "inherited command"])
diff --git a/src/tests/Tests/DocElabExtensions/LocalExtension.lean b/src/tests/VersoTests/DocElabExtensions/LocalExtension.lean
similarity index 100%
rename from src/tests/Tests/DocElabExtensions/LocalExtension.lean
rename to src/tests/VersoTests/DocElabExtensions/LocalExtension.lean
diff --git a/src/tests/Tests/DocElabExtensions/Middle.lean b/src/tests/VersoTests/DocElabExtensions/Middle.lean
similarity index 88%
rename from src/tests/Tests/DocElabExtensions/Middle.lean
rename to src/tests/VersoTests/DocElabExtensions/Middle.lean
index 04092745e..e89ab15a4 100644
--- a/src/tests/Tests/DocElabExtensions/Middle.lean
+++ b/src/tests/VersoTests/DocElabExtensions/Middle.lean
@@ -3,10 +3,7 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Emilio Jesus Gallego Arias
-/
-module
-public import Tests.DocElabExtensions.Define
-
-public section
+import VersoTests.DocElabExtensions.Define
namespace Verso.Tests.DocElabExtensions
diff --git a/src/tests/Tests/DocElabExtensions/Use.lean b/src/tests/VersoTests/DocElabExtensions/Use.lean
similarity index 88%
rename from src/tests/Tests/DocElabExtensions/Use.lean
rename to src/tests/VersoTests/DocElabExtensions/Use.lean
index df894c4b6..de9c01b08 100644
--- a/src/tests/Tests/DocElabExtensions/Use.lean
+++ b/src/tests/VersoTests/DocElabExtensions/Use.lean
@@ -3,11 +3,8 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Emilio Jesus Gallego Arias
-/
-module
-public import Tests.DocElabExtensions.Middle
-public meta import Tests.DocElabExtensions.Middle
-
-public section
+import VersoTests.DocElabExtensions.Middle
+import Errata
namespace Verso.Tests.DocElabExtensions
@@ -50,5 +47,5 @@ Directive body
{inheritedCommand}
:::::::
-#guard importedThroughMiddle
-#guard inheritedDocElabExtensions.toPart.content.size == 4
+#test_guard importedThroughMiddle
+#test_guard inheritedDocElabExtensions.toPart.content.size == 4
diff --git a/src/tests/Tests/DocTerm.lean b/src/tests/VersoTests/DocTerm.lean
similarity index 100%
rename from src/tests/Tests/DocTerm.lean
rename to src/tests/VersoTests/DocTerm.lean
diff --git a/src/tests/Tests/DocVisibility.lean b/src/tests/VersoTests/DocVisibility.lean
similarity index 81%
rename from src/tests/Tests/DocVisibility.lean
rename to src/tests/VersoTests/DocVisibility.lean
index 494118691..0b6ed7bcb 100644
--- a/src/tests/Tests/DocVisibility.lean
+++ b/src/tests/VersoTests/DocVisibility.lean
@@ -4,7 +4,8 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
module
-public meta import Tests.DocVisibility.Doc
+public import Errata
+public meta import VersoTests.DocVisibility.Doc
public section
/-!
@@ -20,5 +21,5 @@ info: Verso.Doc.Part.mk
#[Verso.Doc.Block.para #[Verso.Doc.Inline.text "A paragraph.", Verso.Doc.Inline.linebreak "\n"]]
#[]
-/
-#guard_msgs in
-#eval %doc Tests.DocVisibility.Doc
+#test_msgs in
+#eval %doc VersoTests.DocVisibility.Doc
diff --git a/src/tests/Tests/DocVisibility/Doc.lean b/src/tests/VersoTests/DocVisibility/Doc.lean
similarity index 79%
rename from src/tests/Tests/DocVisibility/Doc.lean
rename to src/tests/VersoTests/DocVisibility/Doc.lean
index c5160020c..6f45376ca 100644
--- a/src/tests/Tests/DocVisibility/Doc.lean
+++ b/src/tests/VersoTests/DocVisibility/Doc.lean
@@ -8,8 +8,8 @@ public import Verso
public meta import Verso
/-!
-This document is deliberately not wrapped in a `public section`, so that `Tests.DocVisibility` can
-check that `#doc` results in a public name.
+This document is deliberately not wrapped in a `public section`, so that `VersoTests.DocVisibility`
+can check that `#doc` results in a public name.
-/
#doc (.none) "Title" =>
diff --git a/src/tests/Tests/DocstringMissing.lean b/src/tests/VersoTests/DocstringMissing.lean
similarity index 97%
rename from src/tests/Tests/DocstringMissing.lean
rename to src/tests/VersoTests/DocstringMissing.lean
index f17777584..22947be77 100644
--- a/src/tests/Tests/DocstringMissing.lean
+++ b/src/tests/VersoTests/DocstringMissing.lean
@@ -4,6 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
module
+import Errata
public import VersoManual
open Lean Elab Command
@@ -25,6 +26,6 @@ Hint: If `Signature.mk` is documented, add `import all VersoManual.Docstring.Bas
Set option 'verso.docstring.allowMissing' to 'true' to allow missing docstrings.
-/
-#guard_msgs in
+#test_msgs in
run_cmd do
discard <| getDocString? (← getEnv) ``Verso.Genre.Manual.Signature.mk
diff --git a/src/tests/Tests/DocstringMissingLegacy.lean b/src/tests/VersoTests/DocstringMissingLegacy.lean
similarity index 91%
rename from src/tests/Tests/DocstringMissingLegacy.lean
rename to src/tests/VersoTests/DocstringMissingLegacy.lean
index 0ba6d699a..f8b9d6310 100644
--- a/src/tests/Tests/DocstringMissingLegacy.lean
+++ b/src/tests/VersoTests/DocstringMissingLegacy.lean
@@ -3,6 +3,7 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
+import Errata
import VersoManual
open Lean Elab Command
@@ -13,7 +14,7 @@ set_option guard_msgs.diff true
/-!
The `import all` hint is specific to module documents, where a plain `import` does not load
docstrings. A non-`module` document loads docstrings from a plain `import`, so the
-diagnostic omits the hint. This is the non-`module` counterpart of `Tests/DocstringMissing.lean`.
+diagnostic omits the hint. This is the non-`module` counterpart of `VersoTests/DocstringMissing.lean`.
-/
/--
@@ -21,6 +22,6 @@ error: 'Verso.Genre.Manual.Signature.mk' is not documented.
Set option 'verso.docstring.allowMissing' to 'true' to allow missing docstrings.
-/
-#guard_msgs in
+#test_msgs in
run_cmd do
discard <| getDocString? (← getEnv) ``Verso.Genre.Manual.Signature.mk
diff --git a/src/tests/Tests/Elab.lean b/src/tests/VersoTests/Elab.lean
similarity index 85%
rename from src/tests/Tests/Elab.lean
rename to src/tests/VersoTests/Elab.lean
index f554454c6..72af1873c 100644
--- a/src/tests/Tests/Elab.lean
+++ b/src/tests/VersoTests/Elab.lean
@@ -3,12 +3,9 @@ Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Rob Simmons
-/
-module
-public import Verso
-public meta import Verso
-public import VersoManual
-public meta import VersoManual
-public section
+import Errata
+import Verso
+import VersoManual
namespace Verso.BlocksTest
open Genre Manual InlineLean
@@ -27,7 +24,7 @@ open Lean
open Doc.Elab
@[role]
-meta def insertSyntaxGivingRiseToMetavariables : RoleExpanderOf Unit
+def insertSyntaxGivingRiseToMetavariables : RoleExpanderOf Unit
| (), _ => do
``(Doc.Inline.text s!"{4}")
@@ -37,7 +34,7 @@ I can {insertSyntaxGivingRiseToMetavariables}[]
:::::::
@[role]
-meta def totallyUndefined : RoleExpanderOf Unit
+def totallyUndefined : RoleExpanderOf Unit
| (), _content => do
`(_)
@@ -49,14 +46,14 @@ context:
docReconstInBlock✝ : Doc.DocReconstruction
⊢ Doc.Inline Doc.Genre.none
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) var8 "My title here" :=
:::::::
Attempting to insert something {totallyUndefined}[]
:::::::
end
-#guard_msgs in
+#test_msgs in
#docs (Manual) novar "My title here" :=
:::::::
A variable like {lean +error}`x`.
diff --git a/src/tests/Tests/ExpanderSignatures.lean b/src/tests/VersoTests/ExpanderSignatures.lean
similarity index 94%
rename from src/tests/Tests/ExpanderSignatures.lean
rename to src/tests/VersoTests/ExpanderSignatures.lean
index 947feaf8a..8214e5ae2 100644
--- a/src/tests/Tests/ExpanderSignatures.lean
+++ b/src/tests/VersoTests/ExpanderSignatures.lean
@@ -4,6 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
module
+public import Errata
public import Verso
public import VersoManual
public meta import Verso
@@ -23,8 +24,8 @@ expander is defined. These tests pin down that the signature is computed correct
particular that defining an expander whose parser is built from `.many`/`partial` combinators does
not crash when the module holding the constant is loaded.
-This file defines the expanders in a `module`, so the parsers are `meta`. `Tests/ExpanderSignaturesLegacy.lean`
-checks the same thing for a non-`module` source file.
+This file defines the expanders in a `module`, so the parsers are `meta`.
+`VersoTests/ExpanderSignaturesLegacy.lean` checks the same thing for a non-`module` source file.
-/
structure ManyArgs where
@@ -68,7 +69,7 @@ Ident
attr : String (key/value)*
```
-/
-#guard_msgs in
+#test_msgs in
run_cmd do
let report (label : String) (s : Option SigDoc) : CommandElabM Unit :=
match s with
diff --git a/src/tests/Tests/ExpanderSignaturesLegacy.lean b/src/tests/VersoTests/ExpanderSignaturesLegacy.lean
similarity index 93%
rename from src/tests/Tests/ExpanderSignaturesLegacy.lean
rename to src/tests/VersoTests/ExpanderSignaturesLegacy.lean
index d35f77b85..bd1003dc2 100644
--- a/src/tests/Tests/ExpanderSignaturesLegacy.lean
+++ b/src/tests/VersoTests/ExpanderSignaturesLegacy.lean
@@ -3,6 +3,7 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
+import Errata
import Verso
import VersoManual
@@ -13,7 +14,7 @@ open Lean Elab Command
open Verso Doc Elab ArgParse
/-!
-The non-`module` counterpart of `Tests/ExpanderSignatures.lean`. The expanders are defined in a
+The non-`module` counterpart of `VersoTests/ExpanderSignatures.lean`. The expanders are defined in a
legacy source file, so the parsers are ordinary (non-`meta`) definitions and the generated
signature constants are not marked `meta`. The signatures must still be computed correctly, and
loading this file must not crash on the `.many` parser's constant.
@@ -60,7 +61,7 @@ Ident
attr : String (key/value)*
```
-/
-#guard_msgs in
+#test_msgs in
run_cmd do
let report (label : String) (s : Option SigDoc) : CommandElabM Unit :=
match s with
diff --git a/src/tests/Tests/ExtensionResolution.lean b/src/tests/VersoTests/ExtensionResolution.lean
similarity index 89%
rename from src/tests/Tests/ExtensionResolution.lean
rename to src/tests/VersoTests/ExtensionResolution.lean
index d3ed01b2a..e7f446bde 100644
--- a/src/tests/Tests/ExtensionResolution.lean
+++ b/src/tests/VersoTests/ExtensionResolution.lean
@@ -3,11 +3,8 @@ Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Emilio J. Gallego Arias
-/
-module
-public import Verso
-public meta import Verso
-
-public section
+import Errata
+import Verso
namespace Verso.ExtensionResolutionTest
set_option guard_msgs.diff true
@@ -26,7 +23,7 @@ Extension resolution has four relevant outcomes:
namespace RoleCases
@[role]
-meta def registered : RoleExpanderOf Unit
+def registered : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "registered-role")
@@ -36,11 +33,11 @@ meta def registered : RoleExpanderOf Unit
:::::::
/-- info: #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "registered-role"]] -/
-#guard_msgs in
+#test_msgs in
#eval roleRegistered.toPart.content
@[role_expander legacyRegistered]
-meta def legacyRegistered : RoleExpander
+def legacyRegistered : RoleExpander
| _, _ => do
pure #[← `(Verso.Doc.Inline.text "legacy-role")]
@@ -50,7 +47,7 @@ meta def legacyRegistered : RoleExpander
:::::::
/-- info: #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "legacy-role"]] -/
-#guard_msgs in
+#test_msgs in
#eval roleLegacyRegistered.toPart.content
def unregistered : RoleExpander
@@ -60,7 +57,7 @@ def unregistered : RoleExpander
/--
error: Declaration `unregistered` can be used as a role expander but is not registered as a role. Register it with `@[role]`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleUnregistered "Unregistered role" :=
:::::::
{unregistered}[]
@@ -71,7 +68,7 @@ def wrongType : Nat := 7
/--
error: Declaration `wrongType` was found but is not registered as a role.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleWrongType "Wrong role type" :=
:::::::
{wrongType}[]
@@ -83,7 +80,7 @@ error: No registered role `registred`.
Hint: Did you mean role `registered`?
registe̲red
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleTypo "Role typo" :=
:::::::
{registred}[]
@@ -95,7 +92,7 @@ error: No registered role `legacyRegistred`.
Hint: Did you mean role `legacyRegistered`?
legacyRegiste̲red
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleLegacyTypo "Legacy role typo" :=
:::::::
{legacyRegistred}[]
@@ -104,7 +101,7 @@ Hint: Did you mean role `legacyRegistered`?
/--
error: No registered role `nothereatallzzzz`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleNoCloseMatch "No close role match" :=
:::::::
{nothereatallzzzz}[]
@@ -127,49 +124,49 @@ because any single-character role is within distance 1 of any single-character t
/--
error: No registered role `q`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) oneCharDistanceNoMatch "One-character distance no match" :=
:::::::
{q}[]
:::::::
@[role]
-meta def r : RoleExpanderOf Unit
+def r : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "one-character-cutoff-role")
@[role]
-meta def ab : RoleExpanderOf Unit
+def ab : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "short-distance-role")
@[role]
-meta def vvv : RoleExpanderOf Unit
+def vvv : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "three-character-cutoff-role")
@[role]
-meta def distanceRegistered : RoleExpanderOf Unit
+def distanceRegistered : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "distance-role")
@[role]
-meta def yyyyy : RoleExpanderOf Unit
+def yyyyy : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "middle-cutoff-role")
@[role]
-meta def zzzzzz : RoleExpanderOf Unit
+def zzzzzz : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "long-cutoff-role")
@[role]
-meta def multiAlpha : RoleExpanderOf Unit
+def multiAlpha : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "multi-alpha-role")
@[role]
-meta def multiAlphi : RoleExpanderOf Unit
+def multiAlphi : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "multi-alphi-role")
@@ -179,7 +176,7 @@ error: No registered role `q`.
Hint: Did you mean role `r`?
q̵r̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) oneCharDistanceMatch "One-character distance match" :=
:::::::
{q}[]
@@ -191,7 +188,7 @@ error: No registered role `ac`.
Hint: Did you mean role `ab`?
ac̵b̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) shortDistanceMatch "Short distance match" :=
:::::::
{ac}[]
@@ -200,7 +197,7 @@ Hint: Did you mean role `ab`?
/--
error: No registered role `zz`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) shortDistanceNoMatch "Short distance no match" :=
:::::::
{zz}[]
@@ -212,7 +209,7 @@ error: No registered role `vww`.
Hint: Did you mean role `vvv`?
vw̵w̵v̲v̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) threeCharDistanceMatch "Three-character distance match" :=
:::::::
{vww}[]
@@ -221,7 +218,7 @@ Hint: Did you mean role `vvv`?
/--
error: No registered role `www`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) threeCharDistanceNoMatch "Three-character distance no match" :=
:::::::
{www}[]
@@ -233,7 +230,7 @@ error: No registered role `yyyxx`.
Hint: Did you mean role `yyyyy`?
yyyx̵x̵y̲y̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) middleDistanceMatch "Middle distance match" :=
:::::::
{yyyxx}[]
@@ -242,7 +239,7 @@ Hint: Did you mean role `yyyyy`?
/--
error: No registered role `yyxxx`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) middleDistanceNoMatch "Middle distance no match" :=
:::::::
{yyxxx}[]
@@ -254,7 +251,7 @@ error: No registered role `zzzaaa`.
Hint: Did you mean role `zzzzzz`?
zzza̵a̵a̵z̲z̲z̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) longBoundaryDistanceMatch "Long boundary distance match" :=
:::::::
{zzzaaa}[]
@@ -263,7 +260,7 @@ Hint: Did you mean role `zzzzzz`?
/--
error: No registered role `zzaaaa`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) longBoundaryDistanceNoMatch "Long boundary distance no match" :=
:::::::
{zzaaaa}[]
@@ -276,7 +273,7 @@ Hint: Did you mean role `multiAlpha`?
• multiAlphx̵a̲
• multiAlphx̵i̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) multiDistanceSuggestions "Multiple distance suggestions" :=
:::::::
{multiAlphx}[]
@@ -288,7 +285,7 @@ error: No registered role `distanceRegistred`.
Hint: Did you mean role `distanceRegistered`?
distanceRegiste̲red
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) longDistanceMatch "Long distance match" :=
:::::::
{distanceRegistred}[]
@@ -297,7 +294,7 @@ Hint: Did you mean role `distanceRegistered`?
/--
error: No registered role `distanceNoMatchzzzz`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) longDistanceNoMatch "Long distance no match" :=
:::::::
{distanceNoMatchzzzz}[]
@@ -308,7 +305,7 @@ end DistanceCases
namespace ShadowSource
@[role]
-meta def shadowedRegistered : RoleExpanderOf Unit
+def shadowedRegistered : RoleExpanderOf Unit
| (), _ => do
`(Verso.Doc.Inline.text "shadowed-role")
@@ -329,7 +326,7 @@ error: No registered role `ShadowSource.shadowedRegistred`.
Hint: Did you mean role `ShadowSource.shadowedRegistered`?
ShadowSource.shadowedRegiste̲red
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleShadowedSuggestion "Shadowed role suggestion" :=
:::::::
{ShadowSource.shadowedRegistred}[]
@@ -341,7 +338,7 @@ error: No registered role `shadowedRegistred`.
Hint: Did you mean role `ShadowSource.shadowedRegistered`?
s̵h̵a̵d̵o̵w̵e̵d̵R̵e̵g̵i̵s̵t̵r̵e̵d̵S̲h̲a̲d̲o̲w̲S̲o̲u̲r̲c̲e̲.̲s̲h̲a̲d̲o̲w̲e̲d̲R̲e̲g̲i̲s̲t̲e̲r̲e̲d̲
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) roleUnqualifiedShadowedSuggestion "Unqualified shadowed role suggestion" :=
:::::::
{shadowedRegistred}[]
@@ -352,7 +349,7 @@ end ShadowUse
namespace CodeBlockCases
@[code_block]
-meta def registeredBlock : CodeBlockExpanderOf Unit
+def registeredBlock : CodeBlockExpanderOf Unit
| (), str => do
`(Verso.Doc.Block.code $(quote str.getString))
@@ -370,7 +367,7 @@ def unregisteredBlock : CodeBlockExpanderOf Unit
/--
error: Declaration `unregisteredBlock` can be used as a code block expander but is not registered as a code block. Register it with `@[code_block]`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) codeBlockUnregistered "Unregistered code block" :=
:::::::
```unregisteredBlock
@@ -383,7 +380,7 @@ def wrongBlockType : Nat := 7
/--
error: Declaration `wrongBlockType` was found but is not registered as a code block.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) codeBlockWrongType "Wrong code block type" :=
:::::::
```wrongBlockType
@@ -397,7 +394,7 @@ error: No registered code block `registeredBlok`.
Hint: Did you mean code block `registeredBlock`?
registeredBloc̲k
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) codeBlockTypo "Code block typo" :=
:::::::
```registeredBlok
@@ -410,7 +407,7 @@ end CodeBlockCases
namespace DirectiveCases
@[directive]
-meta def registeredDirective : DirectiveExpanderOf Unit
+def registeredDirective : DirectiveExpanderOf Unit
| (), blocks => do
let blocks ← blocks.mapM Verso.Doc.Elab.elabBlock
`(Verso.Doc.Block.concat #[$blocks,*])
@@ -430,7 +427,7 @@ def unregisteredDirective : DirectiveExpanderOf Unit
/--
error: Declaration `unregisteredDirective` can be used as a directive expander but is not registered as a directive. Register it with `@[directive]`.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) directiveUnregistered "Unregistered directive" :=
:::::::
:::unregisteredDirective
@@ -443,7 +440,7 @@ def wrongDirectiveType : Nat := 7
/--
error: Declaration `wrongDirectiveType` was found but is not registered as a directive.
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) directiveWrongType "Wrong directive type" :=
:::::::
:::wrongDirectiveType
@@ -457,7 +454,7 @@ error: No registered directive `registeredDirektive`.
Hint: Did you mean directive `registeredDirective`?
registeredDirek̵c̲tive
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) directiveTypo "Directive typo" :=
:::::::
:::registeredDirektive
@@ -470,7 +467,7 @@ end DirectiveCases
namespace BlockCommandCases
@[block_command]
-meta def registeredCommand : BlockCommandOf Unit
+def registeredCommand : BlockCommandOf Unit
| () => do
`(Verso.Doc.Block.para #[Verso.Doc.Inline.text "registered-command"])
@@ -480,7 +477,7 @@ meta def registeredCommand : BlockCommandOf Unit
:::::::
/-- info: #[Verso.Doc.Block.concat #[(Verso.Doc.Block.para #[Verso.Doc.Inline.text "registered-command"])]] -/
-#guard_msgs in
+#test_msgs in
#eval blockCommandRegistered.toPart.content
def fallbackCommand : Verso.Doc.Block Verso.Doc.Genre.none :=
@@ -492,7 +489,7 @@ def fallbackCommand : Verso.Doc.Block Verso.Doc.Genre.none :=
:::::::
/-- info: #[Verso.Doc.Block.para #[Verso.Doc.Inline.text "fallback-command"]] -/
-#guard_msgs in
+#test_msgs in
#eval blockCommandFallback.toPart.content
/--
@@ -501,7 +498,7 @@ error: No registered block command `registeredComand`.
Hint: Did you mean block command `registeredCommand`?
registeredComm̲and
-/
-#guard_msgs in
+#test_msgs in
#docs (.none) blockCommandTypo "Block command typo" :=
:::::::
{registeredComand}
diff --git a/src/tests/Tests/GenericCode.lean b/src/tests/VersoTests/GenericCode.lean
similarity index 95%
rename from src/tests/Tests/GenericCode.lean
rename to src/tests/VersoTests/GenericCode.lean
index 478481550..44f395989 100644
--- a/src/tests/Tests/GenericCode.lean
+++ b/src/tests/VersoTests/GenericCode.lean
@@ -3,10 +3,8 @@ Copyright (c) 2023 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
-module
-public import Verso
-public meta import Verso
-public section
+import Errata
+import Verso
namespace Verso.GenericCodeTest
set_option guard_msgs.diff true
set_option pp.rawOnError true
@@ -41,7 +39,7 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.code "(define (zero f z) z)\n(define (succ n) (lambda (f x) (f (n f z))))\n"]
#[]]
-/
-#guard_msgs in
+#test_msgs in
#eval code1.toPart
/--
info: Verso.Output.Html.tag
@@ -60,7 +58,7 @@ info: Verso.Output.Html.tag
#[]
(Verso.Output.Html.text true "(define (zero f z) z)\n(define (succ n) (lambda (f x) (f (n f z))))\n")])])
-/
-#guard_msgs in
+#test_msgs in
#eval Doc.Genre.none.toHtml (m := Id) {} () () {} {} {} code1.toPart |>.run .empty |>.fst
@@ -94,5 +92,5 @@ info: Verso.Doc.Part.mk
Verso.Doc.Block.code "(define (zero f z) z)\n(define (succ n) (lambda (f x) (f (n f z))))\n"]
#[]]
-/
-#guard_msgs in
+#test_msgs in
#eval code2.toPart
diff --git a/src/tests/Tests/HighlightedToTeX.lean b/src/tests/VersoTests/HighlightedToTeX.lean
similarity index 92%
rename from src/tests/Tests/HighlightedToTeX.lean
rename to src/tests/VersoTests/HighlightedToTeX.lean
index dba4aa078..e223f48c4 100644
--- a/src/tests/Tests/HighlightedToTeX.lean
+++ b/src/tests/VersoTests/HighlightedToTeX.lean
@@ -4,11 +4,12 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: Jason Reed
-/
module
+import Errata
meta import all Verso.Code.HighlightedToTex
open Verso.Doc.TeX (escapeForVerbatim)
open SubVerso.Highlighting
/-- info: "\\symbol{123}\\symbol{124}\\symbol{125}\\symbol{92}" -/
-#guard_msgs in
+#test_msgs in
#eval escapeForVerbatim "{|}\\"
diff --git a/src/tests/Tests/HoverMerge.lean b/src/tests/VersoTests/HoverMerge.lean
similarity index 64%
rename from src/tests/Tests/HoverMerge.lean
rename to src/tests/VersoTests/HoverMerge.lean
index 43fb72a39..542871abf 100644
--- a/src/tests/Tests/HoverMerge.lean
+++ b/src/tests/VersoTests/HoverMerge.lean
@@ -4,6 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
module
+import Errata
import all Verso.Code.Highlighted
meta import Verso.Output.Html
meta import SubVerso.Highlighting
@@ -24,20 +25,20 @@ def tok : Html := .tag "span" #[("class", "token"), ("data-verso-hover", "5")] (
def tokNoHover : Html := .tag "span" #[("class", "token")] (.text true "x")
-- The attribute is taken from a bare element.
-#guard takeAttrs #["data-verso-hover"] tok == (#[("data-verso-hover", "5")], tokNoHover)
+#test_guard takeAttrs #["data-verso-hover"] tok == (#[("data-verso-hover", "5")], tokNoHover)
-- The attribute is found through a wrapping element, such as a link.
-#guard takeAttrs #["data-verso-hover"] (.tag "a" #[("href", "x.html")] tok) ==
+#test_guard takeAttrs #["data-verso-hover"] (.tag "a" #[("href", "x.html")] tok) ==
(#[("data-verso-hover", "5")], .tag "a" #[("href", "x.html")] tokNoHover)
-- Attributes are gathered across the wrappers of a sole element: the hover from the token
-- and the extra links from the link element around it.
-#guard takeAttrs #["data-verso-hover", "data-verso-links"]
+#test_guard takeAttrs #["data-verso-hover", "data-verso-links"]
(.tag "a" #[("data-verso-links", "[]")] tok) ==
(#[("data-verso-links", "[]"), ("data-verso-hover", "5")], .tag "a" #[] tokNoHover)
-- Only the attributes that are present appear in the result.
-#guard takeAttrs #["data-verso-hover", "data-verso-links"]
+#test_guard takeAttrs #["data-verso-hover", "data-verso-links"]
(.tag "a" #[("data-verso-links", "[]")] tokNoHover) ==
(#[("data-verso-links", "[]")], .tag "a" #[] tokNoHover)
@@ -47,54 +48,54 @@ def tokLinked : Html :=
-- Each attribute is taken from the outermost element that carries it, and repeats on
-- elements nested inside stay in place.
-#guard takeAttrs #["data-verso-hover", "data-verso-links"]
+#test_guard takeAttrs #["data-verso-hover", "data-verso-links"]
(.tag "a" #[("data-verso-hover", "9")] tokLinked) ==
(#[("data-verso-hover", "9"), ("data-verso-links", "[2]")],
.tag "a" #[] (.tag "span" #[("class", "token"), ("data-verso-hover", "5")] (.text true "x")))
-#guard takeAttrs #["data-verso-hover", "data-verso-links"]
+#test_guard takeAttrs #["data-verso-hover", "data-verso-links"]
(.tag "a" #[("data-verso-links", "[1]")] tokLinked) ==
(#[("data-verso-links", "[1]"), ("data-verso-hover", "5")],
.tag "a" #[] (.tag "span" #[("class", "token"), ("data-verso-links", "[2]")] (.text true "x")))
-- The outermost attribute wins, and inner ones are left in place.
-#guard takeAttrs #["data-verso-hover"] (.tag "a" #[("data-verso-hover", "9")] tok) ==
+#test_guard takeAttrs #["data-verso-hover"] (.tag "a" #[("data-verso-hover", "9")] tok) ==
(#[("data-verso-hover", "9")], .tag "a" #[] tok)
-- Empty content around a sole element does not block the search.
-#guard takeAttrs #["data-verso-hover"] (.seq #[.text true "", tok, .seq #[]]) ==
+#test_guard takeAttrs #["data-verso-hover"] (.seq #[.text true "", tok, .seq #[]]) ==
(#[("data-verso-hover", "5")], tokNoHover)
-- Adjacent content blocks the search, including whitespace.
-#guard takeAttrs #["data-verso-hover"] (.seq #[tok, .text true "y"]) ==
+#test_guard takeAttrs #["data-verso-hover"] (.seq #[tok, .text true "y"]) ==
(#[], .seq #[tok, .text true "y"])
-#guard takeAttrs #["data-verso-hover"] (.seq #[tok, tokNoHover]) == (#[], .seq #[tok, tokNoHover])
-#guard takeAttrs #["data-verso-hover"] (.seq #[.text true " ", tok]) ==
+#test_guard takeAttrs #["data-verso-hover"] (.seq #[tok, tokNoHover]) == (#[], .seq #[tok, tokNoHover])
+#test_guard takeAttrs #["data-verso-hover"] (.seq #[.text true " ", tok]) ==
(#[], .seq #[.text true " ", tok])
-- Adjacent content inside a wrapper blocks the search.
-#guard takeAttrs #["data-verso-hover"] (.tag "a" #[] (.seq #[tok, tokNoHover])) ==
+#test_guard takeAttrs #["data-verso-hover"] (.tag "a" #[] (.seq #[tok, tokNoHover])) ==
(#[], .tag "a" #[] (.seq #[tok, tokNoHover]))
-- Content without the attributes is unchanged.
-#guard takeAttrs #["data-verso-hover"] tokNoHover == (#[], tokNoHover)
-#guard takeAttrs #["data-verso-hover"] (.text true "x") == (#[], .text true "x")
-#guard takeAttrs #["data-verso-hover"] (.seq #[]) == (#[], .seq #[])
+#test_guard takeAttrs #["data-verso-hover"] tokNoHover == (#[], tokNoHover)
+#test_guard takeAttrs #["data-verso-hover"] (.text true "x") == (#[], .text true "x")
+#test_guard takeAttrs #["data-verso-hover"] (.seq #[]) == (#[], .seq #[])
def hlTok : Highlighted := .token ⟨.keyword none none none, "rfl"⟩
def hlTok' : Highlighted := .token ⟨.keyword none none none, "skip"⟩
-- A sequence around a single element becomes that element, through nesting and empty text.
-#guard (Highlighted.seq #[hlTok]).normalize == hlTok
-#guard (Highlighted.seq #[.seq #[hlTok]]).normalize == hlTok
-#guard (Highlighted.seq #[.text "", hlTok, .seq #[]]).normalize == hlTok
+#test_guard (Highlighted.seq #[hlTok]).normalize == hlTok
+#test_guard (Highlighted.seq #[.seq #[hlTok]]).normalize == hlTok
+#test_guard (Highlighted.seq #[.text "", hlTok, .seq #[]]).normalize == hlTok
-- Whitespace is content, and sequences with several elements keep their structure.
-#guard (Highlighted.seq #[.text " ", hlTok]).normalize == .seq #[.text " ", hlTok]
-#guard (Highlighted.seq #[hlTok, hlTok']).normalize == .seq #[hlTok, hlTok']
+#test_guard (Highlighted.seq #[.text " ", hlTok]).normalize == .seq #[.text " ", hlTok]
+#test_guard (Highlighted.seq #[hlTok, hlTok']).normalize == .seq #[hlTok, hlTok']
-- Normalization reaches inside spans and proof states.
-#guard (Highlighted.span #[] (.seq #[hlTok])).normalize == .span #[] hlTok
-#guard (Highlighted.tactics #[] 5 10 (.seq #[.text "", hlTok])).normalize ==
+#test_guard (Highlighted.span #[] (.seq #[hlTok])).normalize == .span #[] hlTok
+#test_guard (Highlighted.tactics #[] 5 10 (.seq #[.text "", hlTok])).normalize ==
.tactics #[] 5 10 hlTok
end Verso.HoverMergeTest
diff --git a/src/tests/Tests/Html.lean b/src/tests/VersoTests/Html.lean
similarity index 97%
rename from src/tests/Tests/Html.lean
rename to src/tests/VersoTests/Html.lean
index 515cd0150..f8293079c 100644
--- a/src/tests/Tests/Html.lean
+++ b/src/tests/VersoTests/Html.lean
@@ -4,6 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
module
+import Errata
meta import all Verso.Output.Html
namespace Verso.Tests.Html
@@ -21,7 +22,7 @@ info: Verso.Output.Html.tag
#[("charset", "UTF-8"), ("charset", "UTF-8"), ("a", "b"), ("a-b-c", "44"), ("x", "y")]
(Verso.Output.Html.seq #[])
-/
-#guard_msgs in
+#test_msgs in
#eval testAttrs
private def testAttrsAntiquotes :=
@@ -33,7 +34,7 @@ info: Verso.Output.Html.tag
#[("charset", "UTF-8"), ("charset", "UTF-8"), ("a", "b"), ("a-b-c", "44"), ("x", "y")]
(Verso.Output.Html.seq #[])
-/
-#guard_msgs in
+#test_msgs in
#eval testAttrsAntiquotes
private def test : Html := {{
@@ -73,7 +74,7 @@ info: Verso.Output.Html.tag
#[Verso.Output.Html.text true "foo bar", Verso.Output.Html.tag "br" #[] (Verso.Output.Html.seq #[]),
Verso.Output.Html.text true "hey"])])])
-/
-#guard_msgs in
+#test_msgs in
#eval test
private def leanKwTest : Html := {{
@@ -81,7 +82,7 @@ private def leanKwTest : Html := {{
}}
/-- info: Verso.Output.Html.tag "label" #[("for", "foo")] (Verso.Output.Html.text true "Blah") -/
-#guard_msgs in
+#test_msgs in
#eval leanKwTest
@@ -91,7 +92,7 @@ error: `
` doesn't allow contents
Hint: Remove contents
<̵b̵r̵>̵"̵f̵o̵o̵"̵ ̵"̵f̵o̵o̵"̵<̵/̵b̵r̵>̵<̲b̲r̲/̲>̲
-/
-#guard_msgs in
+#test_msgs in
#eval show Html from {{
"foo" "foo" }}
/--
@@ -107,7 +108,7 @@ info: |