Skip to content

Commit 83acf8f

Browse files
committed
Support for REPL-simulated test cases
The implementation is based on the code from Documenter.jl. Allows to define test cases with REPL-simulated syntax: julia> 1 1 This is useful as allows to define new test cases by simply copy pasting real REPL sessions.
1 parent 9d00b17 commit 83acf8f

File tree

2 files changed

+266
-9
lines changed

2 files changed

+266
-9
lines changed

src/NarrativeTest.jl

+94-9
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ struct TestCase <: AbstractTestCase
125125
code::TextBlock
126126
pre::Union{TextBlock,Nothing}
127127
expect::Union{TextBlock,Nothing}
128+
repl::Bool
128129
end
129130

130131
location(test::TestCase) = test.loc
@@ -537,29 +538,36 @@ function parsemd!(stack::Vector{TextBlock})
537538
line = pop!(stack)
538539
if isfence(line)
539540
# Extract a fenced block.
541+
isrepl = false
540542
fenceloc = line.loc
541543
lang = strip(line.val[4:end])
542544
jlstack = TextBlock[]
543545
while !isempty(stack) && !isfence(stack[end])
544-
push!(jlstack, pop!(stack))
546+
block = pop!(stack)
547+
isrepl = isrepl || startswith(block.val, "julia>")
548+
push!(jlstack, block)
545549
end
546550
if isempty(stack)
547551
push!(suite, BrokenTestCase(fenceloc, "incomplete fenced code block"))
548552
else
549553
pop!(stack)
550554
if isempty(lang)
551555
reverse!(jlstack)
552-
append!(suite, parsejl!(jlstack))
556+
append!(suite, isrepl ? parsejlrepl!(jlstack) : parsejl!(jlstack))
553557
end
554558
end
555559
elseif isindent(line) && !isblank(line)
556560
# Extract an indented block.
557-
jlstack = TextBlock[unindent(line)]
561+
block = unindent(line)
562+
isrepl = startswith(block.val, "julia>")
563+
jlstack = TextBlock[block]
558564
while !isempty(stack) && (isindent(stack[end]) || isblank(stack[end]))
559-
push!(jlstack, unindent(pop!(stack)))
565+
block = unindent(pop!(stack))
566+
isrepl = isrepl || startswith(block.val, "julia> ")
567+
push!(jlstack, block)
560568
end
561569
reverse!(jlstack)
562-
append!(suite, parsejl!(jlstack))
570+
append!(suite, isrepl ? parsejlrepl!(jlstack) : parsejl!(jlstack))
563571
elseif isadmonition(line)
564572
# Skip an indented admonition block.
565573
while !isempty(stack) && (isindent(stack[end]) || isblank(stack[end]))
@@ -588,6 +596,78 @@ function parsejl!(stack::Vector{TextBlock})
588596
return suite
589597
end
590598

599+
const PROMPT_REGEX = r"^julia>(?: (.*))?$"
600+
const SOURCE_REGEX = r"^ (.*)$"
601+
602+
function parsejlrepl!(stack::Vector{TextBlock})
603+
reverse!(stack)
604+
code = TextBlock[]
605+
expect = TextBlock[]
606+
buf = IOBuffer()
607+
processing_prompt = false
608+
function savecode!(loc)
609+
data = consumebuf!(buf)
610+
if data !== nothing
611+
push!(code, TextBlock(loc, strip(data)))
612+
else
613+
push!(code, TextBlock(loc, ""))
614+
end
615+
end
616+
function saveexpect!(loc)
617+
data = consumebuf!(buf)
618+
if data !== nothing
619+
push!(expect, TextBlock(loc, rstrip(data)))
620+
end
621+
end
622+
while true
623+
line = popfirst!(stack)
624+
prompt = match(PROMPT_REGEX, line.val)
625+
if prompt === nothing
626+
source = match(SOURCE_REGEX, line.val)
627+
if source === nothing
628+
processing_prompt = false
629+
savecode!(line.loc)
630+
println(buf, rstrip(line.val))
631+
takeuntil!(PROMPT_REGEX, buf, stack)
632+
else
633+
processing_prompt = true
634+
println(buf, source[1])
635+
end
636+
else
637+
processing_prompt = true
638+
saveexpect!(line.loc)
639+
if prompt[1] !== nothing
640+
println(buf, prompt[1])
641+
end
642+
end
643+
if isempty(stack)
644+
processing_prompt ? savecode!(line.loc) : saveexpect!(line.loc)
645+
for _ in length(code):length(expect) + 1
646+
push!(expect, TextBlock(line.loc, ""))
647+
end
648+
break
649+
end
650+
end
651+
[isempty(c.val) ?
652+
BrokenTestCase(c.loc, "empty test case") :
653+
TestCase(c.loc, c, nothing, e, true) for (c, e) in zip(code, expect)]
654+
end
655+
656+
function consumebuf!(buf)
657+
n = bytesavailable(seekstart(buf))
658+
n > 0 ? String(take!(buf)) : nothing
659+
end
660+
661+
function takeuntil!(r, buf, lines::Vector{TextBlock})
662+
while !isempty(lines)
663+
if !occursin(r, lines[1].val)
664+
println(buf, rstrip(popfirst!(lines).val))
665+
else
666+
break
667+
end
668+
end
669+
end
670+
591671
# Extract a test case from Julia source.
592672

593673
function parsecase!(stack::Vector{TextBlock})
@@ -638,7 +718,7 @@ function parsecase!(stack::Vector{TextBlock})
638718
end
639719
end
640720
!isempty(code) || return BrokenTestCase(loc, "missing test code")
641-
return TestCase(loc, collapse(code), collapse(pre), collapse(expect))
721+
return TestCase(loc, collapse(code), collapse(pre), collapse(expect), false)
642722
end
643723

644724
# Run a single test case.
@@ -703,7 +783,11 @@ function runtest(test::TestCase; subs=common_subs(), mod=nothing)
703783
body = asexpr(test.code)
704784
ans = Core.eval(mod, body)
705785
if ans !== nothing && !no_output
706-
Base.invokelatest(show, io, ans)
786+
if test.repl
787+
Base.invokelatest(show, io, "text/plain", ans)
788+
else
789+
Base.invokelatest(show, io, ans)
790+
end
707791
end
708792
end
709793
catch exc
@@ -746,10 +830,11 @@ end
746830
runtest(test::BrokenTestCase; subs=common_subs(), mod=nothing) =
747831
Error(test)
748832

749-
runtest(loc, code; pre=nothing, expect=nothing, subs=common_subs(), mod=nothing) =
833+
runtest(loc, code; pre=nothing, expect=nothing, subs=common_subs(), mod=nothing, repl=false) =
750834
runtest(TestCase(loc, TextBlock(loc, code),
751835
pre !== nothing ? TextBlock(loc, pre) : nothing,
752-
expect !== nothing ? TextBlock(loc, expect) : nothing),
836+
expect !== nothing ? TextBlock(loc, expect) : nothing,
837+
repl),
753838
subs=subs, mod=mod)
754839

755840
# Convert expected output block to a regex.

test/index.md

+172
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,178 @@ It is also an error if a multi-line output block is not closed.
384384
incomplete multiline comment block
385385
=#
386386

387+
It is possible to define tests using syntax simulating REPL session (in fact
388+
REPL session can be copy pasted as-is to become test cases), just prefix a line
389+
in a code block with `julia> `:
390+
391+
suite = parsemd(
392+
@__FILE__,
393+
IOBuffer("""
394+
These test cases are embedded in an indented code block.
395+
396+
julia> (3+4)*6
397+
42
398+
399+
julia> 2+2
400+
5
401+
402+
The following test cases are embedded in a fenced code block.
403+
```
404+
julia> print(2^16)
405+
65526
406+
407+
julia> sqrt(-1)
408+
0.0 + 1.0im
409+
```
410+
"""))
411+
foreach(display, suite)
412+
#=>
413+
Test case at …/index.md:4
414+
(3+4)*6
415+
Expected output:
416+
42
417+
Test case at …/index.md:7
418+
2+2
419+
Expected output:
420+
5
421+
Test case at …/index.md:12
422+
print(2^16)
423+
Expected output:
424+
65526
425+
Test case at …/index.md:15
426+
sqrt(-1)
427+
Expected output:
428+
0.0 + 1.0im
429+
=#
430+
431+
Expected output can be missing:
432+
433+
suite = parsemd(
434+
@__FILE__,
435+
IOBuffer("""
436+
The indented code block below is missing few expected outputs:
437+
438+
julia> 1
439+
440+
julia> 2
441+
2
442+
443+
julia> 3
444+
445+
Now fenced code block with missing few expected outputs:
446+
447+
```
448+
julia> 1
449+
450+
julia> 2
451+
2
452+
453+
julia> 3
454+
```
455+
"""))
456+
foreach(display, suite)
457+
#=>
458+
Test case at …/index.md:4
459+
1
460+
Expected output:
461+
462+
Test case at …/index.md:6
463+
2
464+
Expected output:
465+
2
466+
Test case at …/index.md:9
467+
3
468+
Expected output:
469+
470+
Test case at …/index.md:14
471+
1
472+
Expected output:
473+
474+
Test case at …/index.md:16
475+
2
476+
Expected output:
477+
2
478+
Test case at …/index.md:18
479+
3
480+
Expected output:
481+
482+
=#
483+
484+
REPL-like test cases can span multiple lines as in real REPL if we keep
485+
indentation:
486+
487+
suite = parsemd(
488+
@__FILE__,
489+
IOBuffer("""
490+
Multiline code blocks:
491+
492+
julia> [1 2;
493+
3 4]
494+
2×2 Matrix{Int64}:
495+
1 2
496+
3 4
497+
498+
julia> 1
499+
2
500+
2
501+
502+
julia> "no
503+
output?"
504+
505+
"""))
506+
foreach(display, suite)
507+
#=>
508+
Test case at …/index.md:5
509+
[1 2;
510+
3 4]
511+
Expected output:
512+
2×2 Matrix{Int64}:
513+
1 2
514+
3 4
515+
Test case at …/index.md:11
516+
1
517+
2
518+
Expected output:
519+
2
520+
Test case at …/index.md:15
521+
"no
522+
output?"
523+
Expected output:
524+
=#
525+
526+
Empty prompt with REPL-like test cases is considered as invalid:
527+
528+
suite = parsemd(
529+
@__FILE__,
530+
IOBuffer("""
531+
Some test cases are invalid below as they are missing code
532+
to execute:
533+
534+
julia> 1
535+
536+
julia>
537+
538+
julia> 3
539+
3
540+
541+
julia>
542+
543+
"""))
544+
foreach(display, suite)
545+
#=>
546+
Test case at …/index.md:5
547+
1
548+
Expected output:
549+
550+
Error at …/index.md:7
551+
empty test case
552+
Test case at …/index.md:9
553+
3
554+
Expected output:
555+
3
556+
Error at …/index.md:12
557+
empty test case
558+
=#
387559

388560
## Running one test
389561

0 commit comments

Comments
 (0)