-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
/
Copy pathbuildkitetestjson.jl
167 lines (151 loc) · 5.73 KB
/
buildkitetestjson.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# This file is a part of Julia. License is MIT: https://julialang.org/license
# Convert test(set) results to a Buildkite-compatible JSON representation.
# Based on <https://buildkite.com/docs/test-analytics/importing-json#json-test-results-data-reference>.
module BuildkiteTestJSON
using Test
using Dates
export write_testset_json_files
# Bootleg JSON writer
"""
json_repr(io::IO, value; kwargs...) -> Nothing
Obtain a JSON representation of `value`, and print it to `io`.
This may not be the best, most feature-complete, or fastest implementation.
However, it works for its intended purpose.
"""
function json_repr end
function json_repr(io::IO, val::String; indent::Int=0)
print(io, '"')
escape_string(io, val, ('"',))
print(io, '"')
end
json_repr(io::IO, val::Integer; indent::Int=0) = print(io, val)
json_repr(io::IO, val::Float64; indent::Int=0) = print(io, val)
function json_repr(io::IO, val::AbstractVector; indent::Int=0)
print(io, '[')
for i in eachindex(val)
print(io, '\n', ' '^(indent + 2))
json_repr(io, val[i]; indent=indent+2)
i == lastindex(val) || print(io, ',')
end
print(io, '\n', ' '^indent, ']')
end
function json_repr(io::IO, val::Dict; indent::Int=0)
print(io, '{')
for (i, (k, v)) in enumerate(pairs(val))
print(io, '\n', ' '^(indent + 2))
json_repr(io, string(k))
print(io, ": ")
json_repr(io, v; indent=indent+2)
i === length(val) || print(io, ',')
end
print(io, '\n', ' '^indent, '}')
end
json_repr(io::IO, val::Any; indent::Int=0) = json_repr(io, string(val))
# Test result processing
function result_dict(testset::Test.DefaultTestSet, prefix::String="")
Dict{String, Any}(
"id" => Base.UUID(rand(UInt128)),
"scope" => join((prefix, testset.description), '/'),
"history" => if !isnothing(testset.time_end)
Dict{String, Any}(
"start_at" => testset.time_start,
"end_at" => testset.time_end,
"duration" => testset.time_end - testset.time_start)
else
Dict{String, Any}("start_at" => testset.time_start, "duration" => 0.0)
end)
end
function result_dict(result::Test.Result)
file, line = if !hasproperty(result, :source) || isnothing(result.source)
"unknown", 0
else
something(result.source.file, "unknown"), result.source.line
end
status = if result isa Test.Pass && result.test_type === :skipped
"skipped"
elseif result isa Test.Pass
"passed"
elseif result isa Test.Fail || result isa Test.Error
"failed"
else
"unknown"
end
data = Dict{String, Any}(
"name" => "$(result.test_type): $(result.orig_expr)",
"location" => string(file, ':', line),
"file_name" => file,
"result" => status)
add_failure_info!(data, result)
end
function add_failure_info!(data::Dict{String, Any}, result::Test.Result)
if result isa Test.Fail
data["failure_reason"] = if result.test_type === :test && !isnothing(result.data)
"Evaluated: $(result.data)"
elseif result.test_type === :test_throws_nothing
"No exception thrown"
elseif result.test_type === :test_throws_wrong
"Wrong exception type thrown"
else
"unknown"
end
elseif result isa Test.Error
data["failure_reason"] = if result.test_type === :test_error
if occursin("\nStacktrace:\n", result.backtrace)
err, trace = split(result.backtrace, "\nStacktrace:\n", limit=2)
data["failure_expanded"] =
[Dict{String,Any}("expanded" => split(err, '\n'),
"backtrace" => split(trace, '\n'))]
end
"Exception (unexpectedly) thrown during test"
elseif result.test_type === :test_nonbool
"Expected the expression to evaluate to a Bool, not a $(typeof(result.data))"
elseif result.test_type === :test_unbroken
"Expected this test to be broken, but it passed"
else
"unknown"
end
end
data
end
function collect_results!(results::Vector{Dict{String, Any}}, testset::Test.DefaultTestSet, prefix::String="")
common_data = result_dict(testset, prefix)
result_offset = length(results) + 1
result_counts = Dict{Tuple{String, String}, Int}()
for (i, result) in enumerate(testset.results)
if result isa Test.Result
rdata = result_dict(result)
rid = (rdata["location"], rdata["result"])
if haskey(result_counts, rid)
result_counts[rid] += 1
else
result_counts[rid] = 1
push!(results, merge(common_data, rdata))
end
elseif result isa Test.DefaultTestSet
collect_results!(results, result, common_data["scope"])
end
end
# Modify names to hold `result_counts`
for i in result_offset:length(results)
result = results[i]
rid = (result["location"], result["result"])
if get(result_counts, rid, 0) > 1
result["name"] = replace(result["name"], r"^([^:]):" =>
SubstitutionString("\\1 (x$(result_counts[rid])):"))
end
end
results
end
function write_testset_json_files(dir::String, testset::Test.DefaultTestSet)
data = Dict{String, Any}[]
collect_results!(data, testset)
files = String[]
# Buildkite is limited to 5000 results per file https://buildkite.com/docs/test-analytics/importing-json
for (i, chunk) in enumerate(Iterators.partition(data, 5000))
res_file = joinpath(dir, "results_$i.json")
open(io -> json_repr(io, chunk), res_file, "w")
push!(files, res_file)
end
return files
end
end