-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP/RFC] Rewrite testing infrastructure to live in Base #19567
base: master
Are you sure you want to change the base?
Changes from 7 commits
61c3af4
a0c7f0f
89e8541
ef7ac55
bb070ff
55c2c61
5799d1b
ffa26ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,9 @@ export @testset | |
export @test_approx_eq, @test_approx_eq_eps, @inferred | ||
export detect_ambiguities | ||
export GenericString | ||
export runtests | ||
|
||
include("testdefs.jl") | ||
|
||
#----------------------------------------------------------------------- | ||
|
||
|
@@ -407,7 +410,7 @@ finish(ts::FallbackTestSet) = ts | |
""" | ||
DefaultTestSet | ||
|
||
If using the DefaultTestSet, the test results will be recorded. If there | ||
If using the `DefaultTestSet`, the test results will be recorded. If there | ||
are any `Fail`s or `Error`s, an exception will be thrown only at the end, | ||
along with a summary of the test results. | ||
""" | ||
|
@@ -1075,6 +1078,83 @@ function detect_ambiguities(mods...; imported::Bool=false) | |
return collect(ambs) | ||
end | ||
|
||
""" | ||
build_results_testset(results) | ||
|
||
Construct a testset on the master node which will hold results from all the | ||
test files run on workers and on node1. The loop goes through the results, | ||
inserting them as children of the overall testset if they are testsets, | ||
handling errors otherwise. | ||
|
||
Since the workers don't return information about passing/broken tests, only | ||
errors or failures, those Result types get passed `nothing` for their test | ||
expressions (and expected/received result in the case of Broken). | ||
|
||
If a test failed, returning a `RemoteException`, the error is displayed and | ||
the overall testset has a child testset inserted, with the (empty) Passes | ||
and Brokens from the worker and the full information about all errors and | ||
failures encountered running the tests. This information will be displayed | ||
as a summary at the end of the test run. | ||
|
||
If a test failed, returning an `Exception` that is not a `RemoteException`, | ||
it is likely the julia process running the test has encountered some kind | ||
of internal error, such as a segfault. The entire testset is marked as | ||
Errored, and execution continues until the summary at the end of the test | ||
run, where the test file is printed out as the "failed expression". | ||
""" | ||
function build_results_testset(results) | ||
o_ts = DefaultTestSet("Overall") | ||
push_testset(o_ts) | ||
for res in results | ||
if isa(res[2][1], DefaultTestSet) | ||
push_testset(res[2][1]) | ||
record(o_ts, res[2][1]) | ||
pop_testset() | ||
elseif isa(res[2][1], Tuple{Int,Int}) | ||
fake = DefaultTestSet(res[1]) | ||
for i in 1:res[2][1][1] | ||
record(fake, Pass(:test, nothing, nothing, nothing)) | ||
end | ||
for i in 1:res[2][1][2] | ||
record(fake, Broken(:test, nothing)) | ||
end | ||
push_testset(fake) | ||
record(o_ts, fake) | ||
pop_testset() | ||
elseif isa(res[2][1], RemoteException) | ||
println("Worker $(res[2][1].pid) failed running test $(res[1]):") | ||
Base.showerror(STDOUT,res[2][1].captured) | ||
o_ts.anynonpass = true | ||
if isa(res[2][1].captured.ex, TestSetException) | ||
fake = DefaultTestSet(res[1]) | ||
for i in 1:res[2][1].captured.ex.pass | ||
record(fake, Pass(:test, nothing, nothing, nothing)) | ||
end | ||
for i in 1:res[2][1].captured.ex.broken | ||
record(fake, Broken(:test, nothing)) | ||
end | ||
for t in res[2][1].captured.ex.errors_and_fails | ||
record(fake, t) | ||
end | ||
push_testset(fake) | ||
record(o_ts, fake) | ||
pop_testset() | ||
end | ||
elseif isa(res[2][1], Exception) | ||
# If this test raised an exception that is not a RemoteException, that means | ||
# the test runner itself had some problem, so we may have hit a segfault | ||
# or something similar. Record this testset as Errored. | ||
o_ts.anynonpass = true | ||
fake = DefaultTestSet(res[1]) | ||
record(fake, Error(:test_error, res[1], res[2][1], [])) | ||
push_testset(fake) | ||
record(o_ts, fake) | ||
pop_testset() | ||
end | ||
end | ||
return o_ts | ||
end | ||
|
||
""" | ||
The `GenericString` can be used to test generic string APIs that program to | ||
the `AbstractString` interface, in order to ensure that functions can work | ||
|
@@ -1087,4 +1167,142 @@ Base.convert(::Type{GenericString}, s::AbstractString) = GenericString(s) | |
Base.endof(s::GenericString) = endof(s.string) | ||
Base.next(s::GenericString, i::Int) = next(s.string, i) | ||
|
||
function move_to_node1!(tests, node1, ts) | ||
for t in ts | ||
if t in tests | ||
splice!(tests, findfirst(tests, t)) | ||
push!(node1, t) | ||
end | ||
end | ||
end | ||
|
||
function print_test_statistics(test, test_stats, worker, alignments) | ||
name_str = rpad(test*" ($worker)", alignments[1], " ") | ||
time_str = @sprintf("%7.2f", test_stats[2]) | ||
time_str = rpad(time_str, alignments[2], " ") | ||
gc_str = @sprintf("%5.2f", test_stats[5].total_time/10^9) | ||
gc_str = rpad(gc_str, alignments[3] ," ") | ||
# since there may be quite a few digits in the percentage, | ||
# the left-padding here is less to make sure everything fits | ||
percent_str = @sprintf("%4.1f",100*test_stats[5].total_time/(10^9*test_stats[2])) | ||
percent_str = rpad(percent_str,alignments[4]," ") | ||
alloc_str = @sprintf("%5.2f",test_stats[3]/2^20) | ||
alloc_str = rpad(alloc_str,alignments[5]," ") | ||
rss_str = @sprintf("%5.2f",test_stats[6]/2^20) | ||
rss_str = rpad(rss_str,alignments[6]," ") | ||
print_with_color(:white, name_str, " | ", time_str, " | ", gc_str, " | ", percent_str, " | ", alloc_str, " | ", rss_str, "\n") | ||
end | ||
|
||
function runtests(names=["all"]; test_dir=joinpath(JULIA_HOME, Base.DATAROOTDIR, "julia/test/"), numcores::Int=ceil(Int, Sys.CPU_CORES / 2)) | ||
include(joinpath(test_dir, "choosetests.jl")) | ||
tests, n1_tests, bigmemtests, net_on = Main.choosetests(names) | ||
tests = unique(tests) | ||
n1_tests = unique(n1_tests) | ||
bigmemtests = unique(bigmemtests) | ||
# In a constrained memory environment, run the tests which may need a lot of memory after all others | ||
const max_worker_rss = if haskey(ENV, "JULIA_TEST_MAXRSS_MB") | ||
parse(Int, ENV["JULIA_TEST_MAXRSS_MB"]) * 2^20 | ||
else | ||
typemax(Csize_t) | ||
end | ||
node1_tests = String[] | ||
move_to_node1!(tests, node1_tests, n1_tests) | ||
if max_worker_rss != typemax(Csize_t) | ||
move_to_node1!(tests, node1_tests, bigmemtests) | ||
end | ||
|
||
if haskey(ENV, "JULIA_TEST_EXEFLAGS") | ||
const test_exeflags = `$(Base.shell_split(ENV["JULIA_TEST_EXEFLAGS"]))` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No const There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good |
||
else | ||
const test_exeflags = `--check-bounds=yes --startup-file=no --depwarn=error` | ||
end | ||
|
||
if haskey(ENV, "JULIA_TEST_EXENAME") | ||
const test_exename = `$(Base.shell_split(ENV["JULIA_TEST_EXENAME"]))` | ||
else | ||
const test_exename = `$(joinpath(JULIA_HOME, Base.julia_exename()))` | ||
end | ||
|
||
cd(test_dir) do | ||
n = 1 | ||
if net_on | ||
n = min(numcores, length(tests)) | ||
n > 1 && addprocs(n; exename=test_exename, exeflags=test_exeflags) | ||
BLAS.set_num_threads(1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have to be set back to the default after performing the task that requires a single thread? |
||
end | ||
|
||
@everywhere include("../base/testdefs.jl") | ||
|
||
#pretty print the information about gc and mem usage | ||
name_align = max(length("Test (Worker)"), maximum(map(x -> length(x) + 3 + ndigits(nworkers()), tests))) | ||
elapsed_align = length("Time (s)") | ||
gc_align = length("GC (s)") | ||
percent_align = length("GC %") | ||
alloc_align = length("Alloc (MB)") | ||
rss_align = length("RSS (MB)") | ||
alignments = (name_align, elapsed_align, gc_align, percent_align, alloc_align, rss_align) | ||
print_with_color(:white, rpad("Test (Worker)",name_align," "), " | ") | ||
print_with_color(:white, "Time (s) | GC (s) | GC % | Alloc (MB) | RSS (MB)\n") | ||
results=[] | ||
@sync begin | ||
for p in workers() | ||
@async begin | ||
while length(tests) > 0 | ||
test = shift!(tests) | ||
local resp | ||
try | ||
resp = remotecall_fetch(runtest, p, test) | ||
catch e | ||
resp = [e] | ||
end | ||
push!(results, (test, resp)) | ||
if (isa(resp[end], Integer) && (resp[end] > max_worker_rss)) || isa(resp, Exception) | ||
if n > 1 | ||
rmprocs(p, waitfor=0.5) | ||
p = addprocs(1; exename=test_exename, exeflags=test_exeflags)[1] | ||
remotecall_fetch(()->include("../base/testdefs.jl"), p) | ||
else | ||
# single process testing, bail if mem limit reached, or, on an exception. | ||
isa(resp, Exception) ? rethrow(resp) : error("Halting tests. Memory limit reached : $resp > $max_worker_rss") | ||
end | ||
end | ||
if !isa(resp[1], Exception) | ||
print_test_statistics(test, resp, p, alignments) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
# Free up memory =) | ||
n > 1 && rmprocs(workers(), waitfor=5.0) | ||
for t in node1_tests | ||
# As above, try to run each test | ||
# which must run on node 1. If | ||
# the test fails, catch the error, | ||
# and either way, append the results | ||
# to the overall aggregator | ||
local resp | ||
try | ||
resp = runtest(t) | ||
catch e | ||
resp = [e] | ||
end | ||
push!(results, (t, resp)) | ||
if !isa(resp[1], Exception) | ||
print_test_statistics(t, resp, 1, alignments) | ||
end | ||
end | ||
o_ts = build_results_testset(results) | ||
println() | ||
print_test_results(o_ts,1) | ||
if !o_ts.anynonpass | ||
println(" \033[32;1mSUCCESS\033[0m") | ||
else | ||
println(" \033[31;1mFAILURE\033[0m") | ||
print_test_errors(o_ts) | ||
error() | ||
end | ||
end | ||
end | ||
|
||
end # module |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# This file is a part of Julia. License is MIT: http://julialang.org/license | ||
|
||
function runtest(name, isolate=true) | ||
if isolate | ||
mod_name = Symbol("TestMain_", replace(name, '/', '_')) | ||
m = eval(Main, :(module $mod_name end)) | ||
else | ||
m = Main | ||
end | ||
eval(m, :(using Base.Test)) | ||
ex = quote | ||
@timed @testset $"$name" begin | ||
include($"$name.jl") | ||
end | ||
end | ||
res_and_time_data = eval(m, ex) | ||
rss = Sys.maxrss() | ||
#res_and_time_data[1] is the testset | ||
passes,fails,error,broken,c_passes,c_fails,c_errors,c_broken = Base.Test.get_test_counts(res_and_time_data[1]) | ||
if res_and_time_data[1].anynonpass == false | ||
res_and_time_data = ( | ||
(passes+c_passes,broken+c_broken), | ||
res_and_time_data[2], | ||
res_and_time_data[3], | ||
res_and_time_data[4], | ||
res_and_time_data[5]) | ||
end | ||
vcat(collect(res_and_time_data), rss) | ||
end | ||
|
||
# looking in . messes things up badly | ||
#filter!(x->x!=".", LOAD_PATH) | ||
nothing |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ endif | |
$(TESTS): | ||
@cd $(SRCDIR) && \ | ||
$(ULIMIT_TEST) \ | ||
$(call PRINT_JULIA, $(call spawn,$(JULIA_EXECUTABLE)) --check-bounds=yes --startup-file=no ./runtests.jl $@) | ||
$(call PRINT_JULIA, $(call spawn,$(JULIA_EXECUTABLE)) --check-bounds=yes --startup-file=no -e "Base.Test.runtests(ARGS)" $@) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make sure we still have this file? It's very useful to run the tests without going through makefile in order to specify different options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I can put it back in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That will also un-break Travis and AV, probably 😓 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
|
||
perf: | ||
@$(MAKE) -C $(SRCDIR)/perf all | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these includes should just move out of the function. This function shouldnt call include except on the test file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The includes are in the function in case you want to pass the package's test dir in, and you can have your own
choosetests.jl
inPkg.dir()/test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't sound like a good API. And it also doesn't apply to
testdefs.jl
, which should just be part ofBase
.If a package needs to customize the tests it wants to run, it should just do that in
runtests.jl
and pass the test list toBase.runtests()
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT doing this in
choosetests.jl
instead ofruntests.jl
doesn't really make it any easier for the packages. The most useful part of the basechoosetests.jl
is (IMHO) the--skip
option and theall
"meta"-test and this currently doesn't make it possible for the package to use it. It might be useful to provide a few helper functions for cmdline parsing but I honestly don't think the one we have in base is a particularly good one and packages are better off using sth likeArgParse.jl
for that.Even if a
choosetests
type of interface is wanted, it should be implemented as a callback rather thaninclude
ing a file.