-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
150 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import Base: Info, | ||
shouldlog, handle_message, min_enabled_level, catch_exceptions | ||
import Base: ismatch | ||
|
||
#------------------------------------------------------------------------------- | ||
# Log records | ||
struct LogRecord | ||
level | ||
message | ||
_module | ||
group | ||
id | ||
file | ||
line | ||
kwargs | ||
end | ||
LogRecord(args...; kwargs...) = LogRecord(args..., kwargs) | ||
|
||
#------------------------------------------------------------------------------- | ||
# Logger with extra test-related state | ||
mutable struct TestLogger <: AbstractLogger | ||
logs::Vector{LogRecord} | ||
min_level::LogLevel | ||
catch_exceptions::Bool | ||
shouldlog_args | ||
end | ||
|
||
TestLogger(; min_level=Info, catch_exceptions=false) = TestLogger(LogRecord[], min_level, catch_exceptions, nothing) | ||
min_enabled_level(logger::TestLogger) = logger.min_level | ||
|
||
function shouldlog(logger::TestLogger, level, _module, group, id) | ||
logger.shouldlog_args = (level, _module, group, id) | ||
true | ||
end | ||
|
||
function handle_message(logger::TestLogger, level, msg, _module, | ||
group, id, file, line; kwargs...) | ||
push!(logger.logs, LogRecord(level, msg, _module, group, id, file, line, kwargs)) | ||
end | ||
|
||
# Catch exceptions for the test logger only if specified | ||
catch_exceptions(logger::TestLogger) = logger.catch_exceptions | ||
|
||
function collect_test_logs(f; kwargs...) | ||
logger = TestLogger(; kwargs...) | ||
with_logger(f, logger) | ||
logger.logs | ||
end | ||
|
||
|
||
#------------------------------------------------------------------------------- | ||
# Log testing tools | ||
|
||
""" | ||
@test_logs [log_patterns...] [keywords] expression | ||
Collect a list of log records generated by `expression` using | ||
`collect_test_logs`, and check that they match the sequence `log_patterns`. | ||
The `keywords` provide some simple filtering of log records: the `min_level` | ||
keyword controls the minimum log level which will be collected for the test. | ||
The most useful log pattern is a simple tuple of the form `(level,message)`. | ||
More or less tuple elements may be added corresponding to the arguments to | ||
passed to `AbstractLogger` via the `handle_message` function: | ||
`(level,message,module,group,id,file,line)`. Elements which are present will | ||
be matched pairwise with the log record fields using `==` or `ismatch` when the | ||
pattern field is a `Regex`. | ||
# Examples | ||
Consider a function which logs a warning, and several debug messages: | ||
function foo(n) | ||
@info "Doing foo with n=\$n" | ||
for i=1:n | ||
@debug "Iteration \$i" | ||
end | ||
end | ||
We can test the info message using | ||
@test_logs (Info,"Doing foo with n=2") foo(2) | ||
If we also wanted to test the debug messages, these need to be enabled with the | ||
`min_level` keyword: | ||
@test_logs (Info,"Doing foo with n=2") (Debug,"Iteration 1") (Debug,"Iteration 2") min_level=Debug foo(2) | ||
""" | ||
macro test_logs(exs...) | ||
length(exs) >= 1 || throw(ArgumentError("""`@test_logs` needs at least one arguments. | ||
Usage: `@test_logs [msgs...] expr_to_run`""")) | ||
args = Any[] | ||
kwargs = Any[] | ||
for e in exs[1:end-1] | ||
if e isa Expr && e.head == :(=) | ||
push!(kwargs, Expr(:kw, e.args...)) | ||
else | ||
push!(args, esc(e)) | ||
end | ||
end | ||
# TODO: Better error reporting in @test | ||
ex = quote | ||
@test ismatch_logs($(args...); $(kwargs...)) do | ||
$(esc(exs[end])) | ||
end | ||
end | ||
# Propagate source code location of @test_logs to @test macro | ||
ex.args[2].args[2] = __source__ | ||
ex | ||
end | ||
|
||
function ismatch_logs(f, patterns...; kwargs...) | ||
logs = collect_test_logs(f; kwargs...) | ||
length(logs) == length(patterns) || return false | ||
for (pattern,log) in zip(patterns, logs) | ||
ismatch(pattern, log) || return false | ||
end | ||
return true | ||
end | ||
|
||
logfield_ismatch(a, b) = a == b | ||
logfield_ismatch(r::Regex, b) = ismatch(r, b) | ||
logfield_ismatch(r::Regex, b::Symbol) = ismatch(r, String(b)) | ||
|
||
function ismatch(pattern::Tuple, r::LogRecord) | ||
stdfields = (r.level, r.message, r._module, r.group, r.id, r.file, r.line) | ||
all(logfield_ismatch(p,f) for (p,f) in zip(pattern, stdfields[1:length(pattern)])) | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters