diff --git a/src/ProgressLogging.jl b/src/ProgressLogging.jl index ae037f1..1ce7690 100644 --- a/src/ProgressLogging.jl +++ b/src/ProgressLogging.jl @@ -1,8 +1,8 @@ module ProgressLogging -export @progress +export @progress, @withprogress, @logprogress -using Logging: @logmsg, LogLevel +using Logging: Logging, @logmsg, LogLevel const ProgressLevel = LogLevel(-1) @@ -42,6 +42,60 @@ function progress(f; name = "") end end +const _id_name = gensym(:progress_id) + +""" + @withprogress [name=""] ex + +Create a lexical environment in which [`@logprogress`](@ref) can be used to +emit progress log events without manually specifying the log level and `_id`. + +```julia +@withprogress begin + for i = 1:10 + sleep(0.5) + @logprogress "iterating" progress=i/10 + end +end +``` +""" +macro withprogress(ex1, ex2 = nothing) + _withprogress(ex1, ex2) +end + +_withprogress(ex, ::Nothing) = _withprogress(:(name = ""), ex) +function _withprogress(kwarg, ex) + if !(kwarg.head == :(=) && kwarg.args[1] == :name) + throw(ArgumentError("First expression to @withprogress must be `name=...`. Got: $kwarg")) + end + name = kwarg.args[2] + + @gensym name_var + m = @__MODULE__ + quote + let $_id_name = gensym(:progress_id), + $name_var = $name + $m.@logprogress $name_var progress = NaN + try + $ex + finally + $m.@logprogress $name_var progress = "done" + end + end + end |> esc +end + +""" + @logprogress name progress=value ... + +See [`@withprogress`](@ref). +""" +macro logprogress(name, args...) + quote + $Logging.@logmsg($ProgressLevel, $name, _id = $_id_name, $(args...)) + end |> esc +end + """ @progress [name="", threshold=0.005] for i = ..., j = ..., ... @progress [name="", threshold=0.005] x = [... for i = ..., j = ..., ...] diff --git a/test/test_withprogress_macro.jl b/test/test_withprogress_macro.jl new file mode 100644 index 0000000..4022a72 --- /dev/null +++ b/test/test_withprogress_macro.jl @@ -0,0 +1,71 @@ +module TestWithprogressMacro + +using Logging +using ProgressLogging +using ProgressLogging: ProgressLevel +using Test +using Test: collect_test_logs + +@testset "simple" begin + logs, = collect_test_logs(min_level = ProgressLevel) do + @withprogress @logprogress "hello" progress = 0.1 + end + @test length(logs) == 3 + @test logs[1].kwargs[:progress] === NaN + @test logs[2].kwargs[:progress] === 0.1 + @test logs[3].kwargs[:progress] === "done" + @test length(unique([l.id for l in logs])) == 1 +end + +@testset "with name" begin + logs, = collect_test_logs(min_level = ProgressLevel) do + @withprogress name = "name" @logprogress "hello" progress = 0.1 + end + @test length(logs) == 3 + @test logs[1].kwargs[:progress] === NaN + @test logs[2].kwargs[:progress] === 0.1 + @test logs[3].kwargs[:progress] === "done" + @test logs[1].message === "name" + @test logs[2].message === "hello" + @test logs[3].message === "name" + @test length(unique([l.id for l in logs])) == 1 +end + +@testset "nested" begin + logs, = collect_test_logs(min_level = ProgressLevel) do + @withprogress begin + @logprogress "hello" progress = 0.1 + @withprogress begin + @logprogress "world" progress = 0.2 + end + end + end + + @test length(logs) == 6 + + ids = unique([l.id for l in logs]) + @test length(ids) == 2 + + @test Tuple((l.id, l.message, l.kwargs[:progress]) for l in logs) === ( + (ids[1], "", NaN), + (ids[1], "hello", 0.1), + (ids[2], "", NaN), + (ids[2], "world", 0.2), + (ids[2], "", "done"), + (ids[1], "", "done"), + ) +end + +@testset "invalid input" begin + local err + @test try + @eval @withprogress invalid_argument = "" nothing + catch err + err + end isa Exception # unfortunately `LoadError`, not an `ArgumentError` + msg = sprint(showerror, err) + @test occursin("First expression to @withprogress must be `name=...`.", msg) + @test occursin("invalid_argument", msg) +end + +end # module