Skip to content

Commit fc8f902

Browse files
Merge pull request #53 from argumentcomputer/ap/refactor-lspecIO
chore: change `lspecIO`
2 parents ca8e280 + faeeafe commit fc8f902

File tree

6 files changed

+86
-285
lines changed

6 files changed

+86
-285
lines changed

CI.lean

-33
This file was deleted.

LSpec/Examples.lean

+10-33
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ section LSpec
66
open LSpec
77

88
/- The simplest way to invoke `LSpec` is in a file via the `#lspec` command -/
9-
#lspec test "Nat equality" (45)
9+
#lspec test "Nat equality" (45)
1010

1111
/-
1212
`#lspec` runs a sequence of tests which are encoded with the inductive type `TestSeq` which allows
@@ -18,37 +18,14 @@ for tests to be composable
1818

1919
/-
2020
Tests that can be tested are of the `Testable` typeclass, which have a low-priority instance
21-
`(p : Prop) : Decidable p → Testable p` which can be over-ridden to allow for more intricate
21+
`(p : Prop) : Decidable p → Testable p` which can be over-ridden to allow for more intricate
2222
failure or success messages.
2323
2424
This instance is generic enough that tests like `∀ n, n < 10 → n - 5 < 5` can be evaluated
2525
-/
2626
#lspec test "all lt" $ ∀ n, n < 10 → n - 5 < 5
2727

2828
/-
29-
It is also possible to run tests inside the `IO` monad. The purpose of these tests is to plug in
30-
`LSpec` into a testing script for a `lake script`
31-
-/
32-
33-
def fourIO : IO Nat :=
34-
return 4
35-
36-
def fiveIO : IO Nat :=
37-
return 5
38-
39-
def main := do
40-
let four ← fourIO
41-
let five ← fiveIO
42-
lspecIO $
43-
test "fourIO equals 4" (four = 4) $
44-
test "fiveIO equals 5" (five = 5)
45-
46-
#eval main
47-
-- ✓ fourIO equals 4
48-
-- ✓ fiveIO equals 5
49-
-- 0
50-
51-
/-
5229
There are even more ways to invoke LSpec tests (`lspecEachIO` for example) for more intricate moandic
5330
testing
5431
-/
@@ -72,7 +49,7 @@ example : SampleableExt Nat := by infer_instance
7249
check "mul_comm" $ ∀ n m : Nat, n * m = m * m
7350
-- ? add_comm
7451
-- × mul_comm
75-
52+
7653
-- ===================
7754
-- Found problems!
7855
-- n := 1
@@ -95,22 +72,22 @@ private def mkPairs (as : List α) (bs : List β) : List (α × β) :=
9572
/- An instance of `Shrinkable` is needed -/
9673
open Shrinkable in
9774
instance : Shrinkable Pairs where
98-
shrink := fun p =>
75+
shrink := fun p =>
9976
let shrinkl := shrink p.left
10077
let shrinkr := shrink p.right
10178
mkPairs shrinkl shrinkr |>.map fun (a, b) => ⟨a, b⟩
10279

10380
/-
10481
As is one for `SampleableExt`.
10582
106-
In both of these cases we are using the instances already written for more primitive types like
83+
In both of these cases we are using the instances already written for more primitive types like
10784
`Nat`, but it's possible to write a these instances by hand if we want more fine-grained behavior.
10885
-/
10986
open SampleableExt
11087

11188
def pairsGen : Gen Pairs := return ⟨← Gen.chooseAny Nat, ← Gen.chooseAny Nat⟩
11289

113-
/-
90+
/-
11491
To generate the instance of `SampleableExt α` we use the `mkSelfContained` function which relies only
11592
on having a `Gen α`.
11693
@@ -121,11 +98,11 @@ instance : SampleableExt Pairs := mkSelfContained pairsGen
12198
/- Now we can conduct the tests just as we did before for `Nat` -/
12299
#lspec check "left + 2 is less than right" $ ∀ pair : Pairs, pair.left + 2 ≤ pair.right
123100

124-
/-
125-
You always have to be careful with your implementation for `shrink` and `SampleableExt` because
101+
/-
102+
You always have to be careful with your implementation for `shrink` and `SampleableExt` because
126103
Slimcheck may not flag tests that should fail, in this case `⟨0, 0⟩` should fail the test but
127104
isn't detected
128105
-/
129-
#lspec check "left + right > right" $ ∀ pair : Pairs, pair.left + pair.right > pair.right
106+
#lspec check "left + right > right" $ ∀ pair : Pairs, pair.left + pair.right > pair.right
130107

131-
end SlimCheck
108+
end SlimCheck

LSpec/LSpec.lean

+59-94
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ open SlimCheck Decorations in
9494
Checks a `Checkable` prop. Note that `mk_decorations` is here simply to improve error messages
9595
and if `p` is Checkable, then so is `p'`.
9696
-/
97-
def check (descr : String) (p : Prop)
98-
(next : TestSeq := .done) (p' : Decorations.DecorationsOf p := by mk_decorations) [Checkable p']: TestSeq :=
97+
def check (descr : String) (p : Prop) (next : TestSeq := .done)
98+
(p' : DecorationsOf p := by mk_decorations) [Checkable p'] : TestSeq :=
9999
test descr p' next
100100

101101
inductive ExpectationFailure (exp got : String) : Prop
@@ -131,10 +131,6 @@ def withExceptError (descr : String) (exc : Except ε α) [ToString α]
131131
| .error e => test descr true $ f e
132132
| .ok a => test descr (ExpectationFailure "error _" s!"ok {a}")
133133

134-
end TestSequences
135-
136-
section PureTesting
137-
138134
/-- A generic runner for `TestSeq` -/
139135
def TestSeq.run (tSeq : TestSeq) (indent := 0) : Bool × String :=
140136
let pad := String.mk $ List.replicate indent ' '
@@ -144,18 +140,17 @@ def TestSeq.run (tSeq : TestSeq) (indent := 0) : Bool × String :=
144140
let (pass, msg) := ts.run (indent + 2)
145141
let (b, m) := aux s!"{acc}{pad}{d}:\n{msg}" n
146142
(pass && b, m)
147-
| .individual d _ (.isTrue _) n =>
148-
let (b, m) := aux s!"{acc}{pad}{d}\n" n
149-
(true && b, m)
143+
| .individual d _ (.isTrue _) n => aux s!"{acc}{pad}{d}\n" n
150144
| .individual d _ (.isMaybe msg) n =>
151-
let (b, m) := aux s!"{acc}{pad}? {d}{formatErrorMsg msg}\n" n
152-
(true && b, m)
145+
aux s!"{acc}{pad}? {d}{formatErrorMsg msg}\n" n
153146
| .individual d _ (.isFalse _ msg) n
154147
| .individual d _ (.isFailure msg) n =>
155-
let (b, m) := aux s!"{acc}{pad}× {d}{formatErrorMsg msg}\n" n
156-
(false && b, m)
148+
let (_b, m) := aux s!"{acc}{pad}× {d}{formatErrorMsg msg}\n" n
149+
(false, m)
157150
aux "" tSeq
158151

152+
end TestSequences
153+
159154
/--
160155
Runs a `TestSeq` with an output meant for the Lean Infoview.
161156
This function is meant to be called from a custom command. It runs in
@@ -176,97 +171,67 @@ A custom command to run `LSpec` tests. Example:
176171
macro "#lspec " term:term : command =>
177172
`(#eval LSpec.runInTermElabMAsUnit $term)
178173

179-
end PureTesting
180-
181-
section MonadicTesting
182-
183-
class TestMonadEmit (m) [Monad m] where
184-
emit : String → m Unit
185-
fail : String → m Unit
186-
187-
/-- A monadic runner that emits test outputs as they're produced. -/
188-
def TestSeq.runM (tSeq : TestSeq) (indent := 0) [Monad m] [h : TestMonadEmit m] :
189-
m Bool :=
190-
let pad := String.mk $ List.replicate indent ' '
191-
match tSeq with
192-
| .done => return true
193-
| .group d ts n => do
194-
h.emit s!"{d}:"
195-
let gb ← ts.runM (indent + 2)
196-
return gb && (← n.runM indent)
197-
| .individual d _ (.isTrue _) n => do
198-
h.emit s!"{pad}{d}"
199-
return true && (← n.runM indent)
200-
| .individual d _ (.isMaybe msg) n => do
201-
h.emit s!"{pad}? {d}{formatErrorMsg msg}"
202-
return true && (← n.runM indent)
203-
| .individual d _ (.isFalse _ msg) n
204-
| .individual d _ (.isFailure msg) n => do
205-
let msg := s!"{pad}× {d}{formatErrorMsg msg}"
206-
h.emit msg; h.fail msg -- also emitting messages from failed tests
207-
return false && (← n.runM indent)
208-
209-
class MonadTest (m) [Monad m] (α) where
210-
success : α
211-
failure : α
212-
nEq : success ≠ failure
213-
214-
def succeed [Monad m] [h : MonadTest m α] : m α :=
215-
return h.success
216-
217-
def fail [Monad m] [h : MonadTest m α] : m α :=
218-
return h.failure
219-
220-
/-- Runs a `TestSeq` in a monad with `TestMonadEmit` and `MonadTest`. -/
221-
def lspecM [Monad m] [TestMonadEmit m] [MonadTest m α] (t : TestSeq) : m α := do
222-
if ← t.runM then succeed
223-
else fail
224-
225-
/--
226-
Interspersedly creates a `TestSeq` from each element `β` of a list with a
227-
function `β → m TestSeq` and runs the test sequence.
228-
-/
229-
def lspecEachM [Monad m] [TestMonadEmit m] [MonadTest m α]
230-
(l : List β) (f : β → m TestSeq) : m α := do
231-
let success ← l.foldlM (init := true) fun acc a => do
232-
pure $ acc && (← ( ← f a).runM)
233-
if success then succeed else fail
234-
235-
section IOTesting
236-
237-
instance : TestMonadEmit IO :=
238-
⟨IO.println, IO.eprintln⟩
239-
240-
instance : MonadTest IO UInt32 :=
241-
0, 1, by decide⟩
242-
174+
open Std (HashMap) in
243175
/--
244-
Runs a `TestSeq` with an output meant for the terminal.
176+
Consumes a map of string-keyed test suites and returns a test function meant to
177+
be used via CLI.
245178
246-
This function is designed to be plugged to a `main` function from a Lean file
247-
that can be compiled. Example:
179+
The arguments `args` are matched against the test suite keys. If a key starts
180+
with one of the elements in `args`, then its respective test suite will be
181+
marked to run.
248182
249-
```lean
250-
def main := lspecIO $
251-
test "four equals four" (4 = 4)
252-
```
183+
If the empty list is provided, all test suites will run.
253184
-/
254-
def lspecIO (t : TestSeq) : IO UInt32 :=
255-
lspecM t
185+
def lspecIO (map : HashMap String (List TestSeq)) (args : List String) : IO UInt32 := do
186+
-- Compute the filtered map containing the test suites to run
187+
let filteredMap :=
188+
if args.isEmpty then map
189+
else Id.run do
190+
let mut acc := .empty
191+
for arg in args do
192+
for (key, tSeq) in map do
193+
if key.startsWith arg then
194+
acc := acc.insert key tSeq
195+
pure acc
196+
197+
-- Accumulate error messages
198+
let mut testsWithErrors : HashMap String (Array String) := .empty
199+
for (key, tSeqs) in filteredMap do
200+
IO.println key
201+
for tSeq in tSeqs do
202+
let (success, msg) := tSeq.run (indent := 2)
203+
if success then IO.println msg
204+
else
205+
IO.eprintln msg
206+
if let some msgs := testsWithErrors[key]? then
207+
testsWithErrors := testsWithErrors.insert key $ msgs.push msg
208+
else
209+
testsWithErrors := testsWithErrors.insert key #[msg]
210+
211+
-- Early return 0 when there are no errors
212+
if testsWithErrors.isEmpty then return 0
213+
214+
-- Print error messages and then return 1
215+
IO.eprintln "-------------------------------- Failing tests ---------------------------------"
216+
for (key, msgs) in testsWithErrors do
217+
IO.eprintln key
218+
for msg in msgs do
219+
IO.eprintln msg
220+
return 1
256221

257222
/--
258223
Runs a sequence of tests that are created from a `List α` and a function
259224
`α → IO TestSeq`. Instead of creating all tests and running them consecutively,
260225
this function alternates between test creation and test execution.
261226
262-
It's rather useful for when the test creation process involves heavy
263-
computations in `IO` (e.g. when `f` reads data from files and processes it).
227+
It's useful when the test creation process involves heavy computations in `IO`
228+
(e.g. when `f` reads data from files and processes it).
264229
-/
265-
def lspecEachIO (l : List α) (f : α → IO TestSeq) : IO UInt32 :=
266-
lspecEachM l f
267-
268-
end IOTesting
269-
270-
end MonadicTesting
230+
def lspecEachIO (l : List α) (f : α → IO TestSeq) : IO UInt32 := do
231+
let success ← l.foldlM (init := true) fun acc a => do
232+
match (← f a).run with
233+
| (true, msg) => IO.println msg; pure acc
234+
| (false, msg) => IO.eprintln msg; pure false
235+
if success then return 0 else return 1
271236

272237
end LSpec

Main.lean

-49
This file was deleted.

0 commit comments

Comments
 (0)