Skip to content

Commit fc62dfc

Browse files
committed
Import files from VT100
1 parent df2e9a7 commit fc62dfc

File tree

7 files changed

+335
-0
lines changed

7 files changed

+335
-0
lines changed

.travis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
language: julia
2+
os:
3+
- linux
4+
julia:
5+
- 0.7
6+
- nightly
7+
notifications:
8+
email: false

LICENSE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The TerminalRegressionTests.jl package is licensed under the MIT "Expat" License:
2+
3+
> Copyright (c) 2015-2018: Keno Fischer.
4+
>
5+
> Permission is hereby granted, free of charge, to any person obtaining
6+
> a copy of this software and associated documentation files (the
7+
> "Software"), to deal in the Software without restriction, including
8+
> without limitation the rights to use, copy, modify, merge, publish,
9+
> distribute, sublicense, and/or sell copies of the Software, and to
10+
> permit persons to whom the Software is furnished to do so, subject to
11+
> the following conditions:
12+
>
13+
> The above copyright notice and this permission notice shall be
14+
> included in all copies or substantial portions of the Software.
15+
>
16+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20+
> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21+
> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22+
> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# TerminalRegressionTests - Test your terminal UIs for regressions
2+
3+
[![Build Status](https://travis-ci.org/Keno/VT100.jl.svg?branch=master)](https://travis-ci.org/Keno/TerminalRegressionTests.jl)
4+
5+
This package builds upon the [VT100.jl](https://github.com/Keno/VT100.jl)
6+
package to provide automated testing of terminal based application. Both
7+
plain text and formatted output is supported. Each test consists of
8+
9+
- The system under test (specified as a callback)
10+
- A file specifying the expected output
11+
- A series of input prompts
12+
13+
The main interface is the `automated_test` function, which takes these three
14+
components as arguemnts. There is also the `create_automated_test` function,
15+
which has the same interface, but will create the output file rather than
16+
verifying against it. The operation of the test is fairly simple:
17+
18+
1. An input is popped from the list of inputs
19+
2. The input is provided to the system under test
20+
3. The system under test is allowed to process the input until the system is
21+
done processing the input and has started blocking until new input is
22+
available
23+
4. The output that the system writes is compared to the output file.
24+
5. Repeat
25+
26+
# Usage
27+
28+
Consider the following example:
29+
30+
```
31+
TerminalRegressionTests.automated_test(
32+
joinpath(thisdir,"TRT.multiout"),
33+
["Julia\n","Yes!!\n"]) do emuterm
34+
print(emuterm, "Please enter your name: ")
35+
name = strip(readline(emuterm))
36+
print(emuterm, "\nHello $name. Do you like tests? ")
37+
resp = strip(readline(emuterm))
38+
@assert resp == "Yes!!"
39+
end
40+
```
41+
42+
Note that the callback gets an `emuterm` as an argument. This is an emulated
43+
VT100 terminal and supports the usual operation. Note that this terminal is the
44+
view from the program under test (i.e. reads from this terminal will obtain
45+
the specified input data).

REQUIRE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VT100

src/TerminalRegressionTests.jl

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
module TerminalRegressionTests
2+
using VT100
3+
import REPL
4+
5+
function load_outputs(file)
6+
outputs = String[]
7+
decorators = String[]
8+
is_output = true
9+
is_continuation = false
10+
nlines = 0
11+
for line in readlines(file)
12+
if line[1] == '+'
13+
is_continuation = line[2] != '+'
14+
if is_continuation
15+
until = findfirst(c -> c == '+', line[2:end])+1
16+
nlines = parse(Int, line[2:until - 1])
17+
push!(outputs,
18+
join(split(outputs[end],'\n')[1:end-nlines],'\n'))
19+
else
20+
push!(outputs,"")
21+
end
22+
is_output = true
23+
continue
24+
elseif line[1] == '-'
25+
if is_continuation
26+
push!(decorators,
27+
join(split(decorators[end],'\n')[1:end-nlines],'\n'))
28+
else
29+
push!(decorators,"")
30+
end
31+
is_output = false
32+
continue
33+
elseif line[1] == '|'
34+
array = is_output ? outputs : decorators
35+
array[end] = string(array[end],isempty(array[end]) ? "" : "\n",line[2:end])
36+
else
37+
error("Unrecognized first character \"$(line[1])\"")
38+
end
39+
end
40+
outputs, decorators
41+
end
42+
43+
mutable struct EmulatedTerminal <: REPL.Terminals.UnixTerminal
44+
input_buffer::IOBuffer
45+
out_stream::Base.TTY
46+
pty::VT100.PTY
47+
terminal::VT100.ScreenEmulator
48+
waiting::Bool
49+
step::Condition
50+
filled::Condition
51+
# Yield after every write, e.g. to test for flickering issues
52+
aggressive_yield::Bool
53+
function EmulatedTerminal()
54+
pty = VT100.create_pty(false)
55+
new(
56+
IOBuffer(UInt8[], true, true, true, true, typemax(Int)),
57+
Base.TTY(pty.slave; readable = false), pty,
58+
pty.em, false, Condition(), Condition())
59+
end
60+
end
61+
function Base.wait(term::EmulatedTerminal)
62+
if !term.waiting || bytesavailable(term.input_buffer) != 0
63+
wait(term.step)
64+
end
65+
end
66+
for T in (Vector{UInt8}, Array, AbstractArray, String, Symbol, Any, Char, UInt8)
67+
function Base.write(term::EmulatedTerminal,a::T)
68+
write(term.out_stream, a)
69+
if term.aggressive_yield
70+
notify(term.step)
71+
end
72+
end
73+
end
74+
Base.eof(term::EmulatedTerminal) = false
75+
function Base.read(term::EmulatedTerminal, ::Type{Char})
76+
if bytesavailable(term.input_buffer) == 0
77+
term.waiting = true
78+
notify(term.step)
79+
wait(term.filled)
80+
end
81+
term.waiting = false
82+
read(term.input_buffer, Char)
83+
end
84+
function Base.readuntil(term::EmulatedTerminal, delim::UInt8; kwargs...)
85+
if bytesavailable(term.input_buffer) == 0
86+
term.waiting = true
87+
notify(term.step)
88+
wait(term.filled)
89+
end
90+
term.waiting = false
91+
readuntil(term.input_buffer, delim; kwargs...)
92+
end
93+
REPL.Terminals.raw!(t::EmulatedTerminal, raw::Bool) =
94+
ccall(:jl_tty_set_mode,
95+
Int32, (Ptr{Cvoid},Int32),
96+
t.out_stream.handle, raw) != -1
97+
REPL.Terminals.pipe_reader(t::EmulatedTerminal) = t.input_buffer
98+
REPL.Terminals.pipe_writer(t::EmulatedTerminal) = t.out_stream
99+
100+
function _compare(output, outbuf)
101+
result = outbuf == output
102+
if !result
103+
println("Test failed. Expected result written to expected.out,
104+
actual result written to failed.out")
105+
open("failed.out","w") do f
106+
write(f,outbuf)
107+
end
108+
open("expected.out","w") do f
109+
write(f,output)
110+
end
111+
for (i,c) in enumerate(output)
112+
if c == Char(outbuf[i])
113+
print(Char(c))
114+
elseif c == '\n'
115+
println()
116+
else
117+
printstyled(stdout, "", color=:red)
118+
end
119+
end
120+
error()
121+
end
122+
return result
123+
end
124+
125+
function compare(em, output, decorator = nothing)
126+
buf = IOBuffer()
127+
decoratorbuf = IOBuffer()
128+
VT100.dump(buf,decoratorbuf,em)
129+
outbuf = take!(buf)
130+
decoratorbuf = take!(decoratorbuf)
131+
_compare(Vector{UInt8}(codeunits(output)), outbuf) || return false
132+
decorator === nothing && return true
133+
_compare(Vector{UInt8}(codeunits(decorator)), decoratorbuf)
134+
end
135+
136+
function process_all_buffered(emuterm)
137+
# Since writes to the tty are asynchronous, there's an
138+
# inherent race condition between them being sent to the
139+
# kernel and being available to epoll. We write a sentintel value
140+
# here and wait for it to be read back.
141+
sentinel = Ref{UInt32}(0xffffffff)
142+
ccall(:write, Cvoid, (Cint, Ptr{UInt32}, Csize_t), emuterm.pty.slave, sentinel, sizeof(UInt32))
143+
Base.process_events(false)
144+
# Read until we get our sentinel
145+
while bytesavailable(emuterm.pty.master) < sizeof(UInt32) ||
146+
reinterpret(UInt32, emuterm.pty.master.buffer.data[(emuterm.pty.master.buffer.size-3):emuterm.pty.master.buffer.size])[] != sentinel[]
147+
emuterm.aggressive_yield || yield()
148+
Base.process_events(false)
149+
sleep(0.01)
150+
end
151+
data = IOBuffer(readavailable(emuterm.pty.master)[1:(end-4)])
152+
while bytesavailable(data) > 0
153+
VT100.parse!(emuterm.terminal, data)
154+
end
155+
end
156+
157+
function automated_test(f, outputpath, inputs; aggressive_yield = false)
158+
emuterm = EmulatedTerminal()
159+
emuterm.aggressive_yield = aggressive_yield
160+
emuterm.terminal.warn = true
161+
outputs, decorators = load_outputs(outputpath)
162+
c = Condition()
163+
@async Base.wait_readnb(emuterm.pty.master, typemax(Int64))
164+
yield()
165+
t1 = @async try
166+
f(emuterm)
167+
Base.notify(c)
168+
catch err
169+
Base.showerror(stderr, err, catch_backtrace())
170+
Base.notify_error(c, err)
171+
end
172+
t2 = @async try
173+
for input in inputs
174+
wait(emuterm);
175+
emuterm.aggressive_yield || @assert emuterm.waiting
176+
output = popfirst!(outputs)
177+
decorator = isempty(decorators) ? nothing : popfirst!(decorators)
178+
@assert !eof(emuterm.pty.master)
179+
process_all_buffered(emuterm)
180+
compare(emuterm.terminal, output, decorator)
181+
print(emuterm.input_buffer, input); notify(emuterm.filled)
182+
end
183+
Base.notify(c)
184+
catch err
185+
Base.showerror(stderr, err, catch_backtrace())
186+
Base.notify_error(c, err)
187+
end
188+
while !istaskdone(t1) || !istaskdone(t2)
189+
wait(c)
190+
end
191+
end
192+
193+
function create_automated_test(f, outputpath, inputs; aggressive_yield=false)
194+
emuterm = EmulatedTerminal()
195+
emuterm.aggressive_yield = aggressive_yield
196+
emuterm.terminal.warn = true
197+
c = Condition()
198+
@async Base.wait_readnb(emuterm.pty.master, typemax(Int64))
199+
yield()
200+
t1 = @async try
201+
f(emuterm)
202+
Base.notify(c)
203+
catch err
204+
Base.showerror(stderr, err, catch_backtrace())
205+
Base.notify_error(c, err)
206+
end
207+
t2 = @async try
208+
outs = map(inputs) do input
209+
wait(emuterm);
210+
emuterm.aggressive_yield || @assert emuterm.waiting
211+
process_all_buffered(emuterm)
212+
out = IOBuffer()
213+
decorator = IOBuffer()
214+
VT100.dump(out, decorator, emuterm.terminal)
215+
print(emuterm.input_buffer, input); notify(emuterm.filled)
216+
out, decorator
217+
end
218+
open(outputpath, "w") do io
219+
print(io,"+"^50,"\n",
220+
join(map(outs) do x
221+
sprint() do io
222+
out, dec = x
223+
print(io, "|", replace(String(take!(out)),"\n" => "\n|"))
224+
println(io, "\n", "-"^50)
225+
print(io, "|", replace(String(take!(dec)),"\n" => "\n|"))
226+
end
227+
end, string('\n',"+"^50,'\n')))
228+
end
229+
Base.notify(c)
230+
catch err
231+
Base.showerror(stderr, err, catch_backtrace())
232+
Base.notify_error(c, err)
233+
end
234+
while !istaskdone(t1) || !istaskdone(t2)
235+
wait(c)
236+
end
237+
end
238+
end

test/TRT.multiout

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
++++++++++++++++++++++++++++++++++++++++++++++++++++++
2+
|Please enter your name:
3+
+1++++++++++++++++++++++++++++++++++++++++++++++++++++
4+
|Please enter your name:
5+
|Hello Julia. Do you like tests?
6+
+1++++++++++++++++++++++++++++++++++++++++++++++++++++
7+
|Please enter your name:
8+
|Hello Julia. Do you like tests?
9+
++++++++++++++++++++++++++++++++++++++++++++++++++++++

test/runtests.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using TerminalRegressionTests
2+
3+
const thisdir = dirname(@__FILE__)
4+
TerminalRegressionTests.automated_test(
5+
joinpath(thisdir,"TRT.multiout"),
6+
["Julia\n","Yes!!\n"]) do emuterm
7+
print(emuterm, "Please enter your name: ")
8+
name = strip(readline(emuterm))
9+
print(emuterm, "\nHello $name. Do you like tests? ")
10+
resp = strip(readline(emuterm))
11+
@assert resp == "Yes!!"
12+
end

0 commit comments

Comments
 (0)