Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

LSpec

A testing framework for Lean 4, inspired by Haskell's Hspec package.

Usage

Composing tests

Sequences of tests are represented by the TestSeq datatype. In order to instantiate terms of TestSeq, use the test helper function:

#check
test "Nat equality" (4 = 4) $
test "Nat inequality" (45)
-- test "Nat equality" (4 = 4) (test "Nat inequality" (4 ≠ 5)) : TestSeq

test consumes a description a proposition and a next test The proposition, however, must have its own instance of Testable.

You can also collect TestSeq into conceptual test groups by using the helper function group:

#check
test "Nat equality" (42 = 42) $
group "manual group" $
test "Nat equality inside group" (4 = 4)

The Testable class

Testable is how Lean is instructed to decide whether certain propositions are resolved as true or false.

This is an example of a simple instance for decidability of equalities:

instance (x y : α) [DecidableEq α] [Repr α] : Testable (x = y) :=
if h : x = y then
.isTrue h
else
.isFalse h s!"Not equal: {repr x} and {repr y}"

The custom failure message is optional.

There are more examples of Testable instances in LSpec/Instances.lean.

The user is, of course, free to provide their own instances.

Actually running the tests

The #lspec command

The #lspec command allows you to test interactively in a file.

Examples:

#lspec
test "four equals four" (4 = 4) $
test "five equals five" (5 = 5)
-- ✓ four equals four-- ✓ five equals five

An important note is that a failing test will raise an error, interrupting the building process.

The lspecIO function

lspecIO is meant to be used in files to be compiled and integrated in a testing infrastructure, as shown below.

defaaSuite := [
test "four equals four" (4 = 4)
]
defbbSuite := [
test "five equals five" (5 = 5)
]
defmain := lspecIO $ .ofList [
("aa", aaSuite),
("bb", bbSuite)
]

Once such main function is defined, its respective executable can be tagged as the @[test_driver] in the lakefile. For further information, inspect the docstring of lspecIO.

Integration with SlimCheck

There are 3 main typeclasses associated with any SlimCheck test:

  • Shrinkable : The typeclass that takes a type a : α and returns a List α of elements which should be thought of as being "smaller" than a (in some sense dependent on the type α being considered).
  • SampleableExt : The typeclass of a . This is roughly equivalent to QuickCheck's Arbitrary typeclass.
  • Checkable : The property to be checked by SlimCheck must have a Checkable instance.

In order to use SlimCheck tests for custom data types, the user will need to implement instances of the typeclasses Shrinkable and SampleableExt for the custom types appearing in the properties being tested.

The module LSpec.SlimCheck.Checkable contains may of the useful definitions and instances that can be used to derive a Checkable instance for a wide variety of properties given just the instances above. If all else fails, the user can also define the Checkable instance by hand.

Once this is done a Slimcheck test is evaluated in a similar way to LSpec tests:

#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + n
#lspec check "add_comm" $ ∀ n m : Nat, n + m = m + m
-- × add_comm-- ===================-- Found problems!-- n := 1-- m := 0-- issue: 1 = 0 does not hold-- (0 shrinks)-- -------------------

Integration with Plausible

LSpec also integrates with Lean's Plausible property-based testing library. The Plausible backend lives alongside the SlimCheck-based check/checkIO described above rather than replacing them, so existing SlimCheck tests continue to work unchanged.

Plausible relies on the same core typeclasses as QuickCheck — Shrinkable and SampleableExt to generate and shrink random values — plus Plausible.Testable for the property itself. Instances for the common types (Nat, Int, List, etc.) ship with Plausible, and custom types are supported by providing Shrinkable/SampleableExt instances just as with SlimCheck.

The module LSpec.Plausible exposes two macros:

  • checkPlausible' — a compile-time property test, evaluated during elaboration with a fixed random seed (deterministic across compilations). This is the Plausible-backed counterpart to check'.
  • checkPlausibleIO' — a runtime property test, deferred until the test suite is run. This enables fresh random values on each run and configurable seeds via cfg.randomSeed. This is the Plausible-backed counterpart to checkIO'.

Both macros capture the property syntax so it appears in the output. (Non-syntax-capturing checkPlausible/checkPlausibleIO functions are also available if you don't need the property echoed back.)

A compile-time test with #lspec:

#lspec checkPlausible' "add_comm" (∀ n m : Nat, n + m = m + n)
-- ✓ ∃₁₀₀: "add_comm" (∀ n m : Nat, n + m = m + n)
#lspec checkPlausible' "bad" (∀ n : Nat, n < 5)
-- × ∃¹⁰/₁₀₀: "bad" (∀ n : Nat, n < 5)-- ===================-- Found problems!-- n := 6-- issue: 6 < 5 does not hold-- (0 shrinks)-- -------------------

A runtime test, run via lspecIO. Because checkPlausibleIO' tests are skipped by the pure #lspec runner, they must be executed with lspecIO (or lspecEachIO):

open LSpec
defplausibleTests : TestSeq :=
checkPlausibleIO' "add_comm" (∀ n m : Nat, n + m = m + n)
defmain : IO UInt32 := lspecIO (.ofList [("plausibleTests", [plausibleTests])]) []

Multiple property tests can be sequenced with ++. Note that the '-suffixed macros capture everything up to the end of the line as the property, so to chain them use the non-capturing checkPlausibleIO function (which takes an explicit next argument):

defsuite : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) $
checkPlausibleIO "mul_one" (∀ n : Nat, n * 1 = n)

The '-suffixed macros always use the default configuration. To pass a fixed seed for reproducible runs (or otherwise customise the Plausible.Configuration), call the underlying checkPlausibleIO function directly:

defreproducible : TestSeq :=
checkPlausibleIO "add_comm" (∀ n m : Nat, n + m = m + n) .done { randomSeed := some 42 }

About

A Testing Framework for Lean

Resources

Stars

84 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages