Skip to content

Commit 5505024

Browse files
authored
Merge 5647ec6 into 08e3c2e
2 parents 08e3c2e + 5647ec6 commit 5505024

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed

stdlib/Test/src/logging.jl

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,22 @@ pairwise; use `:any` to check that the pattern matches at least once somewhere
170170
in the sequence.)
171171
172172
The most useful log pattern is a simple tuple of the form `(level,message)`.
173-
A different number of tuple elements may be used to match other log metadata,
173+
A different number of tuple elements may also be used to match other log metadata,
174174
corresponding to the arguments to passed to `AbstractLogger` via the
175-
`handle_message` function: `(level,message,module,group,id,file,line)`.
175+
`handle_message` function: `(level,message,module,group,id,file,line,kwargs)`.
176176
Elements which are present will be matched pairwise with the log record fields
177177
using `==` by default, with the special cases that `Symbol`s may be used for
178178
the standard log levels, and `Regex`s in the pattern will match string or
179179
Symbol fields using `occursin`.
180180
181+
Individual pieces of a log record can be matched by passing a NamedTuple pattern,
182+
matching fields from log metadata, such as: `(;message="...")` or
183+
`(level=:info, kwargs=(;a=1, b=2))`.
184+
185+
To match a log record's keyword arguments, you can use the kwargs field in the pattern,
186+
and provide a NamedTuple or Dict of key-value pairs. The supplied kwargs can be a partial
187+
set, and the log record's kwargs are matched against the provided pairs.
188+
181189
# Examples
182190
183191
Consider a function which logs a warning, and several debug messages:
@@ -219,6 +227,19 @@ patterns and set the `min_level` accordingly:
219227
220228
If you want to test the absence of warnings (or error messages) in
221229
[`stderr`](@ref) which are not generated by `@warn`, see [`@test_nowarn`](@ref).
230+
231+
If you only want to test for a given field, and don't care about the others, you can do
232+
this by passing a NamedTuple for the pattern. (Don't forget the `;` if you only have a
233+
single field). For example, if you only want to match the message:
234+
235+
@test_logs (; message="hi") @info "hi"
236+
@test_logs (; message="hi") @warn "hi"
237+
238+
To test a log message's key=value pairs, you can use the `kwargs` field. Only supplied
239+
key,value pairs are tested:
240+
241+
@test_logs (level=:info, message="hi", kwargs=(; x=2)) @info("hi", x=2)
242+
@test_logs (level=:info, message="hi", kwargs=(; x=2)) @info("hi", x=2, y=3)
222243
"""
223244
macro test_logs(exs...)
224245
length(exs) >= 1 || throw(ArgumentError("""`@test_logs` needs at least one arguments.
@@ -285,12 +306,33 @@ logfield_contains(a, r::Regex) = occursin(r, a)
285306
logfield_contains(a::Symbol, r::Regex) = occursin(r, String(a))
286307
logfield_contains(a::LogLevel, b::Symbol) = a == parse_level(b)
287308
logfield_contains(a, b::Ignored) = true
309+
logfield_contains(a::NamedTuple, b) = logfield_contains(pairs(a), b)
310+
logfield_contains(a::NamedTuple, b::Ignored) = true # method amibguity resolution
311+
function logfield_contains(a::Base.Pairs, pattern)
312+
pattern = pairs(pattern)
313+
for (k, bv) in pattern
314+
av = get(a, k, :__test_logfield_key_not_found)
315+
if av === :__test_logfield_key_not_found
316+
return false
317+
end
318+
logfield_contains(av, bv) || return false
319+
end
320+
return true
321+
end
288322

289323
function occursin(pattern::Tuple, r::LogRecord)
290-
stdfields = (r.level, r.message, r._module, r.group, r.id, r.file, r.line)
324+
stdfields = (r.level, r.message, r._module, r.group, r.id, r.file, r.line, r.kwargs)
291325
all(logfield_contains(f, p) for (f, p) in zip(stdfields[1:length(pattern)], pattern))
292326
end
293327

328+
function occursin(pattern::NamedTuple, r::LogRecord)
329+
fieldnames = keys(pattern)
330+
stdfields = getfield.(Ref(r), fieldnames)
331+
vals = values(pattern)
332+
all(logfield_contains(f, p) for (f, p) in zip(stdfields, vals))
333+
end
334+
335+
294336
"""
295337
@test_deprecated [pattern] expression
296338

stdlib/Test/test/runtests.jl

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,83 @@ erronce() = @error "an error" maxlog=1
10021002
@test startswith(fails[4].value, "ErrorException")
10031003
end
10041004

1005+
@testset "@test_logs named tuple patterns" begin
1006+
@test_logs (;level=:info) @info("hi")
1007+
@test_logs (;message="hi") @info("hi")
1008+
@test_logs (;level=:info, message="hi") @info("hi")
1009+
1010+
fails = @testset NoThrowTestSet "test failures" begin
1011+
@test_logs (;level=:warn) @info("hi")
1012+
@test_logs (;message="hey") @info("hi")
1013+
@test_logs (;level=:info, message="hey") @info("hi")
1014+
@test_logs (;level=:warn, message="hi") @info("hi")
1015+
end
1016+
@test all(f->f isa Test.LogTestFailure, fails)
1017+
1018+
fails = @testset NoThrowTestSet "badly constructed patterns" begin
1019+
# Bad field names:
1020+
@test_logs (;nonexistant=:warn, message="hi") @info("hi")
1021+
@test_logs (;nonexistant=:warn) @info("hi")
1022+
# Forgot semicolon with single-field pattern:
1023+
@test_logs (message=2) @info("2")
1024+
# Wrong pattern type
1025+
@test_logs (:message => 2) @info("2")
1026+
@test_logs Dict(:message => 2) @info("2")
1027+
end
1028+
# These are all "Error During Test"s
1029+
@test all(f->!(f isa Test.LogTestFailure), fails)
1030+
end
1031+
1032+
@testset "@test_logs kwargs" begin
1033+
function foo1()
1034+
@info("hi", x=2, y=3)
1035+
end
1036+
@test_logs (;level=:info, kwargs=(;x=2, y=3)) foo1()
1037+
@test_logs (;level=:info, kwargs=(;x=2)) foo1()
1038+
1039+
@test_logs (;kwargs=(;x=2)) foo1()
1040+
1041+
# Test with the fully specified LogRecord pattern:
1042+
@test_logs (:info, "hi", @__MODULE__, Test.Ignored(), Test.Ignored(), r"", Test.Ignored(), (;y=3)) foo1()
1043+
1044+
# Test using Dict pattern instead of NamedTuple:
1045+
@test_logs (;kwargs=Dict(:x => 2)) foo1()
1046+
@test_logs (;kwargs=Dict(:x => 2, :y => 3)) foo1()
1047+
1048+
function foo2()
1049+
@info("hi", x=2, y=3)
1050+
@warn("bye", x=2, y=10)
1051+
end
1052+
@test_logs (;level=:info, kwargs=(;x=2, y=3)) match_mode=:any foo2()
1053+
@test_logs (;level=:info, kwargs=(;x=2)) match_mode=:any foo2()
1054+
@test_logs (;level=:warn, kwargs=(;x=2, y=10)) match_mode=:any foo2()
1055+
@test_logs (;level=:warn, kwargs=(;x=2)) match_mode=:any foo2()
1056+
1057+
# Both logs have x=2
1058+
@test_logs (;kwargs=(;x=2)) (;kwargs=(;x=2)) match_mode=:all foo2()
1059+
@test_logs (;kwargs=(;x=2)) (;kwargs=(;x=2, y=10)) match_mode=:all foo2()
1060+
@test_logs (;kwargs=(;x=2, y=3)) (;kwargs=(;x=2, y=10)) match_mode=:all foo2()
1061+
1062+
# Test failures
1063+
fails = @testset NoThrowTestSet "test failures" begin
1064+
function foo3()
1065+
@info("hi", x=2, y=3)
1066+
end
1067+
1068+
# Expecting log z=0, but not found:
1069+
@test_logs (;level=:info, kwargs=(;x=2, y=3, z=0)) foo3()
1070+
@test_logs (;level=:info, kwargs=(;z=0)) foo3()
1071+
# Wrong values for kwargs:
1072+
@test_logs (;level=:info, kwargs=(;x=1, y=3)) foo3()
1073+
@test_logs (;level=:info, kwargs=(;x=1)) foo3()
1074+
@test_logs (;kwargs=(;x=1, y=3)) foo3()
1075+
@test_logs (;kwargs=(;x=1)) foo3()
1076+
# Wrong message, correct kwargs:
1077+
@test_logs (;message="hey", kwargs=(;x=1)) foo3()
1078+
end
1079+
@test all(f->f isa Test.LogTestFailure, fails)
1080+
end
1081+
10051082
let code = quote
10061083
function newfunc()
10071084
42

0 commit comments

Comments
 (0)