diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 45f8f3b..8baee3e 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -280,7 +280,7 @@ function print_test_failed(record::AbstractTestRecord, wrkr, test, ctx::TestIOCo if ctx.verbose init_time_str = @sprintf("%7.2f", base.total_time - base.time) - printstyled(ctx.stdout, lpad(init_time_str, ctx.elapsed_align + 1, " "), " │ ", color = :red) + printstyled(ctx.stderr, lpad(init_time_str, ctx.elapsed_align + 1, " "), " │ ", color = :red) end failed_str = "failed at $(now())\n" @@ -313,6 +313,14 @@ function print_test_crashed(::Type{<:AbstractTestRecord}, wrkr, test, ctx::TestI end end +# Truncate `line` to at most `max_width` characters, appending "..." when truncated. +function truncate_line(line::AbstractString, max_width::Int) + if length(line) > max_width + line = first(line, max(0, max_width - 3)) * "..." + end + return line +end + # Adapted from `Malt._stdio_loop` function stdio_loop(worker::Malt.Worker, io::Lockable) Threads.@spawn while !eof(worker.stdout) && Malt.isrunning(worker) @@ -1191,9 +1199,7 @@ function _runtests(mod::Module, args::ParsedArgs; line2 = "Running: " * join(status_parts, ", ") ## truncate max_width = displaysize(io_ctx.stdout)[2] - if length(line2) > max_width - line2 = line2[1:max_width-3] * "..." - end + line2 = truncate_line(line2, max_width) # line 3: progress + ETA line3 = "Progress: $completed/$total tests completed" diff --git a/test/runtests.jl b/test/runtests.jl index 1037322..1cb7314 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -346,6 +346,30 @@ end @test contains(str, "1 == 2") end +@testset "failed test row printed entirely to stderr" begin + testsuite = Dict( + "failing test" => quote + @test 1 == 2 + end + ) + + # use separate streams: the whole row of a failed test must go to stderr + out = IOBuffer() + err = IOBuffer() + @test_throws Test.FallbackTestSetException("Test run finished with errors") begin + runtests(ParallelTestRunner, ["--verbose"]; testsuite, stdout=out, stderr=err) + end + + outs = String(take!(out)) + errs = String(take!(err)) + m = match(r"failing test[^\n]*failed at", errs) + @test m !== nothing + # in verbose mode the row has three cell separators (time, init time) on + # stderr; the init-time cell used to leak to stdout instead + @test m !== nothing && count("│", m.match) == 3 + @test !contains(outs, "failed at") +end + @testset "nested failure" begin testsuite = Dict( "nested" => quote @@ -884,6 +908,26 @@ end @test any(contains("--color=yes"), exe.exec) end +@testset "truncate_line" begin + # short lines are untouched + @test ParallelTestRunner.truncate_line("short", 80) == "short" + @test ParallelTestRunner.truncate_line("x"^80, 80) == "x"^80 + + truncated = ParallelTestRunner.truncate_line("x"^100, 80) + @test length(truncated) == 80 + @test endswith(truncated, "...") + + # multi-byte characters must not break the cut: with byte indexing these + # would throw a StringIndexError when the cut lands mid-character + truncated = ParallelTestRunner.truncate_line("t" * "α"^100, 80) + @test length(truncated) == 80 + @test endswith(truncated, "...") + + truncated = ParallelTestRunner.truncate_line("€"^100, 40) + @test length(truncated) == 40 + @test endswith(truncated, "...") +end + @testset "TestHistoryEntry" begin flow = ParallelTestRunner.TestHistoryEntry(1,true) fhigh = ParallelTestRunner.TestHistoryEntry(10,true)