Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions shared/coverage_counts.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Line hit counts, and the one place they have to be narrowed.
#
# Julia's coverage counters are 64 bit whatever the platform's word size is, and
# `jl_write_coverage_data` writes out what they hold — a line in a hot loop passes
# `typemax(Int32)` easily. The vendored `CoverageTools` stores a count as a `CovCount`,
# which is `Union{Nothing,Int}`, so on a 32 bit run there is nowhere to put such a value:
# reading the file back with `CoverageTools.LCOV.readfile` threw
#
# OverflowError: overflow parsing "2345022144"
#
# and, because coverage is collected while a test item is running, that error was reported
# as a failure of whatever item happened to be running at the time.
#
# `packages/` holds git subtrees that are never edited by hand, so the file is read here
# instead, and a count is narrowed only where it crosses into one of those vendored
# structures. One that does not fit saturates rather than throwing: knowing a line ran at
# least 2147483647 times is worth more than losing the run over the exact figure.

# Stands in for "not instrumentable" while counts are accumulated, so that the vector can
# stay concretely typed. No real count can reach it.
const _COUNT_ABSENT = typemin(Int64)

"""
saturating_count(n)

Narrow a 64 bit hit count to what a vendored `CoverageTools.CovCount` can hold. A no-op on a
64 bit platform, where `Int` is already `Int64`.
"""
saturating_count(n::Integer) = Int(clamp(n, typemin(Int), typemax(Int)))
saturating_count(::Nothing) = nothing

"""
read_lcov_counts(path) -> Vector{CoverageTools.FileCoverage}

Read an LCOV info file into the `FileCoverage` entries the rest of the coverage path expects.

This is `CoverageTools.LCOV.readfile` with the counts accumulated in `Int64` and narrowed by
[`saturating_count`](@ref) on the way into the `CovCount` vector.
"""
function read_lcov_counts(path::AbstractString)
files = Tuple{String,Vector{Int64}}[]
counts = nothing

for line in eachline(path)
if startswith(line, "end_of_record")
counts = nothing
elseif (m = match(r"^SF:(.+)", line)) !== nothing
counts = Int64[]
push!(files, (String(m[1]), counts))
elseif (m = match(r"^DA:(\d+),(-?\d+)(,[^,\s]+)?", line)) !== nothing
counts === nothing && continue

ln = parse(Int64, m[1])
da = parse(Int64, m[2])
ln > 0 || continue

if length(counts) < ln
filled = length(counts)
resize!(counts, ln)
fill!(view(counts, (filled + 1):ln), _COUNT_ABSENT)
end

counts[ln] = counts[ln] == _COUNT_ABSENT ? da : counts[ln] + da
end
end

return [
CoverageTools.FileCoverage(
filename,
"",
CoverageTools.CovCount[i == _COUNT_ABSENT ? nothing : saturating_count(i) for i in counts]
)
for (filename, counts) in files
]
end
6 changes: 4 additions & 2 deletions shared/testserver_protocol.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,17 @@ TestMessage(message, location) = TestMessage(message, missing, missing, location
timeoutMs::Union{Missing,Float64}
end

# `Int64` rather than `Int`: a controller and a test process can run at different word
# sizes, and Julia's coverage counters are 64 bit on both.
struct FileCoverage <: JSONRPC.Outbound
uri::String
coverage::Vector{Union{Int,Nothing}}
coverage::Vector{Union{Int64,Nothing}}
end

function FileCoverage(d::Dict)
return FileCoverage(
d["uri"],
Union{Int,Nothing}[i for i in d["coverage"]]
Union{Int64,Nothing}[i for i in d["coverage"]]
)
end

Expand Down
1 change: 1 addition & 0 deletions src/TestItemControllers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export write_junit_xml, write_lcov
include("json_protocol.jl")
include("../shared/testserver_protocol.jl")
include("../shared/urihelper.jl")
include("../shared/coverage_counts.jl")

include("datatypes.jl")
include("results.jl")
Expand Down
7 changes: 6 additions & 1 deletion src/testitemcontroller.jl
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
end

# Shutdown all processes
for (pid, ps) in c.test_processes

Check notice on line 234 in src/testitemcontroller.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_binding

Variable has been assigned but not used.
if state(ps.fsm) != ProcessDead
_shutdown_test_process!(c, ps)
end
Expand Down Expand Up @@ -920,7 +920,12 @@
_record_testitem_result!(c, msg.testitem_id, :passed, msg.duration)

if msg.coverage !== nothing
append!(tr.coverage, map(i -> CoverageTools.FileCoverage(uri2filepath(i.uri), "", i.coverage), msg.coverage))
# `saturating_count` because `CoverageTools.CovCount` is vendored and cannot be
# widened: a 64 bit test process can report a count this controller's `Int` does
# not hold when the two run at different word sizes.
append!(tr.coverage, map(msg.coverage) do i
CoverageTools.FileCoverage(uri2filepath(i.uri), "", CoverageTools.CovCount[saturating_count(n) for n in i.coverage])
end)
end
else
_log_unexpected_missing_work(tr, msg.testitem_id, msg.testprocess_id, test_env_id, "passed")
Expand Down
36 changes: 36 additions & 0 deletions test/test_coverage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,39 @@ end
@test cov[transform_line] !== nothing && cov[transform_line] > 0
end
end

@testitem "Coverage counts wider than the platform's Int" begin
# Julia's coverage counters are 64 bit on every platform, and `jl_write_coverage_data`
# writes out what they hold — a line in a hot loop passes `typemax(Int32)` easily.
# Reading that back used to throw an `OverflowError` on a 32 bit run, reported as a
# failure of whatever test item was running when coverage was collected.
using TestItemControllers: read_lcov_counts, saturating_count

wide = Int64(typemax(Int32)) + 1
lcov = tempname() * ".info"
hot = joinpath(@__DIR__, "hot.jl")

try
write(lcov, "SF:$hot\nDA:1,$wide\nDA:2,0\nDA:4,1\nend_of_record\n")

file_coverage = read_lcov_counts(lcov)

@test length(file_coverage) == 1
@test file_coverage[1].filename == hot

cov = file_coverage[1].coverage
@test length(cov) == 4
@test cov[1] == saturating_count(wide)
@test cov[2] == 0
@test cov[3] === nothing
@test cov[4] == 1
finally
rm(lcov, force=true)
end

# The count survives intact where `Int` is 64 bit, and saturates rather than throwing
# where it is not — the vendored `CoverageTools.CovCount` is a `Union{Nothing,Int}` and
# cannot be widened from here
@test saturating_count(wide) == (Int === Int64 ? wide : typemax(Int))
@test saturating_count(nothing) === nothing
end
5 changes: 3 additions & 2 deletions testprocess/TestItemServer/src/TestItemServer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ module TestItemServer
include("pkg_imports.jl")

import .JSONRPC: @dict_readable
import .CoverageTools: LCOV, amend_coverage_from_src!
import .CoverageTools: amend_coverage_from_src!
import .CancellationTokens: CancellationToken
import Test, Pkg, Sockets
import Logging
import Profile

include("../../../shared/testserver_protocol.jl")
include("../../../shared/coverage_counts.jl")

"""
A crash-reporting handler that returns instead of ending the process, or `nothing` when
Expand Down Expand Up @@ -304,7 +305,7 @@ function collect_coverage_data!(coverage_results, roots)
lcov_filename = tempname() * ".info"
@ccall jl_write_coverage_data(lcov_filename::Cstring)::Cvoid
cov_info = try
LCOV.readfile(lcov_filename)
read_lcov_counts(lcov_filename)
finally
rm(lcov_filename)
end
Expand Down
Loading