|
| 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 |
0 commit comments