From 6d125bbb3bb4d4f4c7aa650483902fd814c3b59f Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Fri, 17 Jul 2026 21:54:20 +0200 Subject: [PATCH] Support Julia 1.12 terminal input --- .github/workflows/CI.yml | 4 +-- Project.toml | 4 +-- src/TerminalRegressionTests.jl | 66 +++++++++++++++++++++++++++------- test/runtests.jl | 26 ++++++++++++++ 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7a70a99..2294d76 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - channel: ['1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11'] + channel: ['1.10', '1.11', '1.12', 'nightly'] os: [ubuntu-latest] steps: - uses: actions/checkout@v6 @@ -29,4 +29,4 @@ jobs: ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 \ No newline at end of file + - uses: julia-actions/julia-runtest@v1 diff --git a/Project.toml b/Project.toml index 28e67da..bc41e9b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TerminalRegressionTests" uuid = "98bfdc55-cc95-5876-a49a-74609291cbe0" author = ["Keno Fischer "] -version = "0.2.2" +version = "0.3.0" [deps] DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" @@ -14,7 +14,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] DeepDiffs = "1" VT100 = "0.3" -julia = "1.1" +julia = "1.10" [targets] test = ["Test"] diff --git a/src/TerminalRegressionTests.jl b/src/TerminalRegressionTests.jl index 8217db4..e76eedd 100644 --- a/src/TerminalRegressionTests.jl +++ b/src/TerminalRegressionTests.jl @@ -3,6 +3,32 @@ module TerminalRegressionTests using DeepDiffs import REPL + @static if VERSION >= v"1.12-" + const EmulatedCondition = Threads.Condition + + function wait_condition(condition) + lock(condition) + try + return wait(condition) + finally + unlock(condition) + end + end + + function notify_condition(condition) + lock(condition) + try + return notify(condition) + finally + unlock(condition) + end + end + else + const EmulatedCondition = Condition + wait_condition(condition) = wait(condition) + notify_condition(condition) = notify(condition) + end + function load_outputs(file) outputs = String[] decorators = String[] @@ -44,11 +70,13 @@ module TerminalRegressionTests mutable struct EmulatedTerminal <: REPL.Terminals.UnixTerminal input_buffer::IOBuffer out_stream::Base.TTY - pty::VT100.PTY + # `VT100.PTY` is Unix-only. Keep the field untyped so this module can + # still precompile on Windows, where terminal regression tests are skipped. + pty terminal::VT100.ScreenEmulator waiting::Bool - step::Condition - filled::Condition + step::EmulatedCondition + filled::EmulatedCondition # Yield after every write, e.g. to test for flickering issues aggressive_yield::Bool function EmulatedTerminal() @@ -56,30 +84,42 @@ module TerminalRegressionTests new( IOBuffer(UInt8[]; read = true, write = true, append = true, truncate = true, maxsize = typemax(Int)), Base.TTY(pty.slave), pty, - pty.em, false, Condition(), Condition() + pty.em, false, EmulatedCondition(), EmulatedCondition() ) end end + if isdefined(REPL.LineEdit, :hascolor) + @eval REPL.LineEdit.hascolor(::EmulatedTerminal) = true + end function Base.wait(term::EmulatedTerminal) if !term.waiting || bytesavailable(term.input_buffer) != 0 - wait(term.step) + wait_condition(term.step) end end for T in (Vector{UInt8}, Array, AbstractArray, String, Symbol, Any, Char, UInt8) function Base.write(term::EmulatedTerminal,a::T) b = write(term.out_stream, a) if term.aggressive_yield - notify(term.step) + notify_condition(term.step) end return b end end Base.eof(term::EmulatedTerminal) = false + function Base.peek(term::EmulatedTerminal, ::Type{T}=UInt8) where {T} + if bytesavailable(term.input_buffer) == 0 + term.waiting = true + notify_condition(term.step) + wait_condition(term.filled) + end + term.waiting = false + peek(term.input_buffer, T) + end function Base.read(term::EmulatedTerminal, ::Type{Char}) if bytesavailable(term.input_buffer) == 0 term.waiting = true - notify(term.step) - wait(term.filled) + notify_condition(term.step) + wait_condition(term.filled) end term.waiting = false read(term.input_buffer, Char) @@ -87,8 +127,8 @@ module TerminalRegressionTests function Base.readuntil(term::EmulatedTerminal, delim::UInt8; kwargs...) if bytesavailable(term.input_buffer) == 0 term.waiting = true - notify(term.step) - wait(term.filled) + notify_condition(term.step) + wait_condition(term.filled) end term.waiting = false readuntil(term.input_buffer, delim; kwargs...) @@ -145,7 +185,7 @@ module TerminalRegressionTests function process_all_buffered(emuterm) # Since writes to the tty are asynchronous, there's an # inherent race condition between them being sent to the - # kernel and being available to epoll. We write a sentintel value + # kernel and being available to epoll. We write a sentinel value # here and wait for it to be read back. sentinel = Ref{UInt32}(0xffffffff) ccall(:write, Cvoid, (Cint, Ptr{UInt32}, Csize_t), emuterm.pty.slave, sentinel, sizeof(UInt32)) @@ -187,7 +227,7 @@ module TerminalRegressionTests @assert !eof(emuterm.pty.master) process_all_buffered(emuterm) cmp(emuterm.terminal, output, decorator) - print(emuterm.input_buffer, input); notify(emuterm.filled) + print(emuterm.input_buffer, input); notify_condition(emuterm.filled) end Base.notify(c) catch err @@ -222,7 +262,7 @@ module TerminalRegressionTests out = IOBuffer() decorator = IOBuffer() VT100.dump(out, decorator, emuterm.terminal) - print(emuterm.input_buffer, input); notify(emuterm.filled) + print(emuterm.input_buffer, input); notify_condition(emuterm.filled) out, decorator end open(outputpath, "w") do io diff --git a/test/runtests.jl b/test/runtests.jl index d35759e..3a84496 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,31 @@ using TerminalRegressionTests using Test +import REPL + +@testset "EmulatedTerminal" begin + emuterm = TerminalRegressionTests.EmulatedTerminal() + if isdefined(REPL.LineEdit, :hascolor) + @test REPL.LineEdit.hascolor(emuterm) + end + try + peek_task = @async peek(emuterm) + wait_task = @async wait(emuterm) + wait_done = timedwait(() -> istaskdone(wait_task), 10) == :ok + @test wait_done + wait_done && fetch(wait_task) + @test emuterm.waiting + print(emuterm.input_buffer, 'x') + TerminalRegressionTests.notify_condition(emuterm.filled) + peek_done = timedwait(() -> istaskdone(peek_task), 10) == :ok + @test peek_done + if peek_done + @test fetch(peek_task) == UInt8('x') + @test read(emuterm, Char) == 'x' + end + finally + finalize(emuterm.pty) + end +end TerminalRegressionTests.automated_test( joinpath(@__DIR__, "TRT.multiout"),