Skip to content
Open
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
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ julia = "1.9"
[extras]
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"
ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Arrow", "Tar", "Test"]
test = ["Arrow", "Pkg", "Tar", "Test"]
72 changes: 72 additions & 0 deletions test/json_trim_public_entrypoints.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using JSON

const ARRAY_JSON = "[1,2,3]"
const STRING_JSON = "\"Ada\""
const TRIM_FIXTURE_DIR = joinpath(@__DIR__, "trim")

JSON.@nonstruct struct TrimCode
value::String
end

JSON.lower(x::TrimCode) = x.value

function checked(cond::Bool, msg::String)::Nothing
cond || error(msg)
return nothing
end

function exercise_lazy_entrypoints()::Nothing
checked(JSON.lazy(ARRAY_JSON) isa JSON.LazyValue, "lazy failed")
checked(JSON.lazyfile(joinpath(TRIM_FIXTURE_DIR, "value.json")) isa JSON.LazyValue, "lazyfile failed")
return nothing
end

function exercise_parse_entrypoints()::Nothing
# Materializing JSON.parse currently pulls in verifier failures in parser
# and StructUtils error paths, so keep this read workload to trim-safe
# lazy entrypoints until those verifier issues can be chased down.
checked(JSON.lazy("7") isa JSON.LazyValue, "numeric lazy detection failed")
checked(JSON.lazy(IOBuffer(STRING_JSON)) isa JSON.LazyValue, "IO lazy detection failed")
return nothing
end

function exercise_write_entrypoints()::Nothing
obj = JSON.Object{String, Int}("score" => 7)
obj[:score] = 10
checked(obj.score == 10, "Object property access failed")
checked(haskey(obj, "score"), "Object setindex! failed")
delete!(obj, :score)
checked(!haskey(obj, "score"), "Object delete! failed")

checked(JSON.json([1, 2, 3]) == ARRAY_JSON, "json string output failed")

io = IOBuffer()
JSON.json(io, [1, 2, 3]; pretty = 2)
checked(String(take!(io)) == "[\n 1,\n 2,\n 3\n]", "pretty IO json output failed")

print_io = IOBuffer()
JSON.print(print_io, [1, 2, 3], 2)
checked(String(take!(print_io)) == "[\n 1,\n 2,\n 3\n]", "JSON.print failed")

jsonlines = JSON.json([[1], [2]]; jsonlines = true)
checked(jsonlines == "[1]\n[2]\n", "jsonlines write failed")
checked(JSON.json(JSON.JSONText("{\"raw\":true}")) == "{\"raw\":true}", "JSONText write failed")
checked(JSON.json(JSON.Null()) == "null", "JSON.Null write failed")
checked(JSON.json(TrimCode("beta")) == "\"beta\"", "custom lower write failed")
return nothing
end

function run_json_trim_public_entrypoints()::Nothing
exercise_lazy_entrypoints()
exercise_parse_entrypoints()
exercise_write_entrypoints()
return nothing
end

function @main(args::Vector{String})::Cint
_ = args
run_json_trim_public_entrypoints()
return 0
end

Base.Experimental.entrypoint(main, (Vector{String},))
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ include(joinpath(dirname(pathof(JSON)), "../test/json.jl"))
if Sys.WORD_SIZE == 64
include(joinpath(dirname(pathof(JSON)), "../test/arrow.jl"))
end
include(joinpath(dirname(pathof(JSON)), "../test/trim_compile_tests.jl"))

function tar_files(tarball::String)
data = Dict{String, Vector{UInt8}}()
Expand Down
2 changes: 2 additions & 0 deletions test/trim/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2"
1 change: 1 addition & 0 deletions test/trim/value.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"Ada"
175 changes: 175 additions & 0 deletions test/trim_compile_tests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using Test
using JSON
import Pkg

const _TRIM_SUPPORTED = VERSION >= v"1.12.0-rc1"
const _JULIAC_ENTRYPOINT_EXPR = "using JuliaC; if isdefined(JuliaC, :main); JuliaC.main(ARGS); else JuliaC._main_cli(ARGS); end"
const _TRIM_COMPILE_TIMEOUT_S = 300.0
const _TRIM_RUN_TIMEOUT_S = 60.0

function _json_project_path()::String
return normpath(joinpath(dirname(pathof(JSON)), ".."))
end

function _prepare_trim_project(project_path::String, trim_project::String)::Nothing
mkpath(trim_project)
cp(joinpath(@__DIR__, "trim", "Project.toml"), joinpath(trim_project, "Project.toml"))
original_project = Base.active_project()
try
Pkg.activate(trim_project)
Pkg.develop(Pkg.PackageSpec(path = project_path))
Pkg.instantiate()
finally
if original_project !== nothing
Pkg.activate(dirname(original_project))
end
end
return nothing
end

function _run_command_with_timeout(cmd::Cmd; timeout_s::Float64, log_label::String)
output_path = tempname()
out = open(output_path, "w")
exit_code = -1
timed_out = false
try
proc = run(pipeline(ignorestatus(cmd), stdout = out, stderr = out); wait = false)
timed_out = _wait_process_with_timeout!(proc; timeout_s, log_label)
exit_code = something(proc.exitcode, -1)
finally
close(out)
end
output = try
read(output_path, String)
catch
""
finally
rm(output_path; force = true)
end
return exit_code, output, timed_out
end

function _wait_process_with_timeout!(proc::Base.Process; timeout_s::Float64, log_label::String)::Bool
started_at = time()
next_log_at = started_at + 10.0
while Base.process_running(proc)
now = time()
if now - started_at >= timeout_s
try
kill(proc)
catch
end
return true
end
if now >= next_log_at
elapsed = round(now - started_at; digits = 1)
println("[trim] $(log_label) WAIT $(elapsed)s")
flush(stdout)
next_log_at = now + 10.0
end
sleep(0.1)
end
try
wait(proc)
catch
end
return false
end

function _trim_timeout_error(kind::String, script_file::String, output::String = "")
msg = "trim $(kind) timed out for $(script_file)"
if !isempty(output)
msg = string(msg, "\n---- captured output ----\n", output, "\n---- end captured output ----")
end
throw(ArgumentError(msg))
end

function _maybe_print_output(header::String, output::String)::Nothing
isempty(output) && return nothing
println(header)
println(output)
println("---- end output ----")
return nothing
end

function _run_trim_compile(trim_project::String, script_path::String, output_name::String)
julia_exe = joinpath(Sys.BINDIR, Base.julia_exename())
cmd = `$julia_exe --startup-file=no --history-file=no --code-coverage=none --project=$trim_project -e $(_JULIAC_ENTRYPOINT_EXPR) -- --output-exe $output_name --project=$trim_project --experimental --trim=safe $script_path`
return _run_command_with_timeout(cmd; timeout_s = _TRIM_COMPILE_TIMEOUT_S, log_label = "compile")
end

function _run_trim_executable(run_path::String)
return _run_command_with_timeout(`$(abspath(run_path))`; timeout_s = _TRIM_RUN_TIMEOUT_S, log_label = "run")
end

function _parse_trim_verify_totals(output::String)
m = match(r"Trim verify finished with\s+(\d+)\s+errors,\s+(\d+)\s+warnings\.", output)
m === nothing && return nothing
return parse(Int, m.captures[1]), parse(Int, m.captures[2])
end

function _count_trim_verify_messages(output::String)::Tuple{Int, Int}
errors = length(collect(eachmatch(r"Verifier error #\d+:", output)))
warnings = length(collect(eachmatch(r"Verifier warning #\d+:", output)))
return errors, warnings
end

function _run_trim_case(trim_project::String, script_file::String, output_name::String)::Nothing
script_path = joinpath(@__DIR__, script_file)
@test isfile(script_path)
println("[trim] compile START $(script_file)")
start_t = time()
mktempdir() do tmpdir
cd(tmpdir) do
exit_code, output, timed_out = _run_trim_compile(trim_project, script_path, output_name)
timed_out && _trim_timeout_error("compile", script_file, output)
totals = _parse_trim_verify_totals(output)
trim_errors, trim_warnings = if totals === nothing
fallback = _count_trim_verify_messages(output)
if exit_code == 0 && fallback == (0, 0)
fallback
else
error("failed to parse trim verifier summary:\n$output")
end
else
totals
end
if trim_errors > 0 || trim_warnings > 0
_maybe_print_output("---- trim compile output ($(script_file)) ----", output)
end
@test trim_errors == 0
@test trim_warnings == 0
output_path = Sys.iswindows() ? "$(output_name).exe" : output_name
@test exit_code == 0
@test isfile(output_path)
run_exit, run_output, run_timed_out = _run_trim_executable(output_path)
run_timed_out && _trim_timeout_error("executable run", script_file, run_output)
if run_exit != 0
_maybe_print_output("---- trim executable output ($(script_file)) ----", run_output)
end
@test run_exit == 0
end
end
println("[trim] compile DONE $(script_file) ($(round(time() - start_t; digits = 2))s)")
return nothing
end

@testset "Trim compile" begin
if Sys.iswindows()
println("[trim] skip Windows: JuliaC trim compilation is currently too slow or stalls on Windows CI")
@test true
elseif Sys.WORD_SIZE != 64
println("[trim] skip 32-bit Julia: JuliaC trim compilation is only covered on 64-bit test jobs")
@test true
elseif !_TRIM_SUPPORTED
println("[trim] skip Julia < 1.12: JuliaC trim compilation is unavailable")
@test true
else
project_path = _json_project_path()
mktempdir() do tmpdir
trim_project = joinpath(tmpdir, "trim_project")
_prepare_trim_project(project_path, trim_project)
_run_trim_case(trim_project, "json_trim_public_entrypoints.jl", "json_trim_public_entrypoints")
end
end
end
Loading