Skip to content

Commit 6f7cfa7

Browse files
authored
Collect test results into BuildKit-compatible JSON (#53145)
1 parent fc06291 commit 6f7cfa7

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
# Buildkite: Ignore the entire .buildkite directory
4040
/.buildkite
4141

42+
# Builtkite: json test data
43+
/test/results.json
44+
4245
# Buildkite: Ignore the unencrypted repo_key
4346
repo_key
4447

test/buildkitetestjson.jl

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
# Convert test(set) results to a Buildkit-compatible JSON representation.
4+
# Based on <https://buildkite.com/docs/test-analytics/importing-json#json-test-results-data-reference>.
5+
6+
module BuildKiteTestJSON
7+
8+
using Test
9+
using Dates
10+
11+
export write_testset_json
12+
13+
# Bootleg JSON writer
14+
15+
"""
16+
json_repr(io::IO, value; kwargs...) -> Nothing
17+
18+
Obtain a JSON representation of `value`, and print it to `io`.
19+
20+
This may not be the best, most feature-complete, or fastest implementation.
21+
However, it works for its intended purpose.
22+
"""
23+
function json_repr end
24+
25+
function json_repr(io::IO, val::String; indent::Int=0)
26+
print(io, '"')
27+
escape_string(io, val, ('"',))
28+
print(io, '"')
29+
end
30+
json_repr(io::IO, val::Integer; indent::Int=0) = print(io, val)
31+
json_repr(io::IO, val::Float64; indent::Int=0) = print(io, val)
32+
function json_repr(io::IO, val::Vector; indent::Int=0)
33+
print(io, '[')
34+
for elt in val
35+
print(io, '\n', ' '^(indent + 2))
36+
json_repr(io, elt; indent=indent+2)
37+
elt === last(val) || print(io, ',')
38+
end
39+
print(io, '\n', ' '^indent, ']')
40+
end
41+
function json_repr(io::IO, val::Dict; indent::Int=0)
42+
print(io, '{')
43+
for (i, (k, v)) in enumerate(pairs(val))
44+
print(io, '\n', ' '^(indent + 2))
45+
json_repr(io, string(k))
46+
print(io, ": ")
47+
json_repr(io, v; indent=indent+2)
48+
i === length(val) || print(io, ',')
49+
end
50+
print(io, '\n', ' '^indent, '}')
51+
end
52+
json_repr(io::IO, val::Any; indent::Int=0) = json_repr(io, string(val))
53+
54+
# Test result processing
55+
56+
function result_dict(testset::Test.DefaultTestSet, prefix::String="")
57+
Dict{String, Any}(
58+
"id" => Base.UUID(rand(UInt128)),
59+
"scope" => join((prefix, testset.description), '/'),
60+
"history" => if !isnothing(testset.time_end)
61+
Dict{String, Any}(
62+
"start_at" => testset.time_start,
63+
"end_at" => testset.time_end,
64+
"duration" => testset.time_end - testset.time_start)
65+
else
66+
Dict{String, Any}("start_at" => testset.time_start, "duration" => 0.0)
67+
end)
68+
end
69+
70+
function result_dict(result::Test.Result)
71+
file, line = if !hasproperty(result, :source) || isnothing(result.source)
72+
"unknown", 0
73+
else
74+
something(result.source.file, "unknown"), result.source.line
75+
end
76+
status = if result isa Test.Pass && result.test_type === :skipped
77+
"skipped"
78+
elseif result isa Test.Pass
79+
"passed"
80+
elseif result isa Test.Fail || result isa Test.Error
81+
"failed"
82+
else
83+
"unknown"
84+
end
85+
data = Dict{String, Any}(
86+
"name" => "$(result.test_type): $(result.orig_expr)",
87+
"location" => string(file, ':', line),
88+
"file_name" => file,
89+
"result" => status)
90+
add_failure_info!(data, result)
91+
end
92+
93+
function add_failure_info!(data::Dict{String, Any}, result::Test.Result)
94+
if result isa Test.Fail
95+
data["failure_reason"] = if result.test_type === :test && !isnothing(result.data)
96+
"Evaluated: $(result.data)"
97+
elseif result.test_type === :test_throws_nothing
98+
"No exception thrown"
99+
elseif result.test_type === :test_throws_wrong
100+
"Wrong exception type thrown"
101+
else
102+
"unknown"
103+
end
104+
elseif result isa Test.Error
105+
data["failure_reason"] = if result.test_type === :test_error
106+
if occursin("\nStacktrace:\n", result.backtrace)
107+
err, trace = split(result.backtrace, "\nStacktrace:\n", limit=2)
108+
data["failure_expanded"] = Dict{String, Any}(
109+
"expanded" => split(err, '\n'),
110+
"backtrace" => split(trace, '\n'))
111+
end
112+
"Exception (unexpectedly) thrown during test"
113+
elseif result.test_type === :test_nonbool
114+
"Expected the expression to evaluate to a Bool, not a $(typeof(result.data))"
115+
elseif result.test_nonbool === :test_unbroken
116+
"Expected this test to be broken, but it passed"
117+
else
118+
"unknown"
119+
end
120+
end
121+
data
122+
end
123+
124+
function collect_results!(results::Vector{Dict{String, Any}}, testset::Test.DefaultTestSet, prefix::String="")
125+
common_data = result_dict(testset, prefix)
126+
for (i, result) in enumerate(testset.results)
127+
if result isa Test.Result
128+
push!(results, merge(common_data, result_dict(result)))
129+
elseif result isa Test.DefaultTestSet
130+
collect_results!(results, result, common_data["scope"])
131+
end
132+
end
133+
results
134+
end
135+
136+
function write_testset_json(io::IO, testset::Test.DefaultTestSet)
137+
data = Dict{String, Any}[]
138+
collect_results!(data, testset)
139+
json_repr(io, data)
140+
end
141+
142+
end

test/runtests.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ using Base: Experimental
99

1010
include("choosetests.jl")
1111
include("testenv.jl")
12+
include("buildkitetestjson.jl")
13+
14+
using .BuildKiteTestJSON
1215

1316
(; tests, net_on, exit_on_error, use_revise, seed) = choosetests(ARGS)
1417
tests = unique(tests)
@@ -424,6 +427,10 @@ cd(@__DIR__) do
424427
Test.record(o_ts, fake)
425428
Test.pop_testset()
426429
end
430+
431+
Base.get_bool_env("CI", false) &&
432+
open(io -> write_testset_json(io, o_ts), "results.json", "w")
433+
427434
Test.TESTSET_PRINT_ENABLE[] = true
428435
println()
429436
# o_ts.verbose = true # set to true to show all timings when successful

0 commit comments

Comments
 (0)