# This file is a part of Julia. License is MIT: https://julialang.org/license

using Test, Random, Serialization, Base64
using Base.ScopedValues: with

@test isempty(Test.detect_closure_boxes(Serialization))

# Check that serializer hasn't gone out-of-frame
@test Serialization.sertag(Symbol) == 1
@test Serialization.sertag(()) == 68
@test Serialization.sertag(false) == 76

function create_serialization_stream(f::Function)
    s = IOBuffer()
    f(s)
    close(s)
end

# Tags
create_serialization_stream() do s
    Serialization.writetag(s, Serialization.sertag(Bool))
    @test take!(s)[end] == UInt8(Serialization.sertag(Bool))
end

create_serialization_stream() do s
    Serialization.write_as_tag(s, Serialization.sertag(Bool))
    @test take!(s)[end] == UInt8(Serialization.sertag(Bool))
end

create_serialization_stream() do s
    Serialization.write_as_tag(s, Serialization.sertag(Symbol))
    data = take!(s)
    @test data[end-1] == 0x00
    @test data[end] == UInt8(Serialization.sertag(Symbol))
end

# SubString

create_serialization_stream() do s
    ss = Any[SubString("12345", 2, 4),
             SubString(GenericString("12345"), 2, 4),
             SubString(s"12345", 2, 4),
             SubString(GenericString(s"12345"), 2, 4),]
    for x in ss
        serialize(s, x)
    end
    seek(s, 0)
    for x in ss
        y = deserialize(s)
        @test x == y
        @test typeof(x) == typeof(y)
    end
end

# Boolean & Empty & Nothing
create_serialization_stream() do s
    serialize(s, true)
    serialize(s, ())
    serialize(s, nothing)
    seek(s, 0)
    @test deserialize(s) === true
    @test deserialize(s) === ()
    @test deserialize(s) === nothing
end

# Ptr
create_serialization_stream() do s
    serialize(s, C_NULL)
    seekstart(s)
    @test deserialize(s) === C_NULL
end

# Integer
create_serialization_stream() do s
    serialize(s, 0x1)
    serialize(s, 32)
    serialize(s, 33)
    seek(s,0)
    @test deserialize(s) === 0x01
    @test deserialize(s) === 32
    @test deserialize(s) === 33
end

# Tuple
create_serialization_stream() do s
    tpl = (0x1,'a')
    serialize(s, tpl)

    len = 257
    lt = ntuple(i->0x1, len)
    serialize(s, lt)

    serialize(s, Tuple{})

    seek(s, 0)
    @test deserialize(s) === tpl
    @test deserialize(s) === lt
    @test deserialize(s) === Tuple{}
end

# Dict
create_serialization_stream() do s
    dct = Dict("A"=>1, "B"=>2)
    serialize(s, dct)
    seek(s, 0)
    @test deserialize(s) == dct
end

# Symbol
create_serialization_stream() do s
    gensym(len) = Symbol(repeat("A", len))

    smbl = gensym(1)
    serialize(s, smbl)

    smbl2 = gensym(10)
    serialize(s, smbl2)

    smbl3 = gensym(257)
    serialize(s, smbl3)

    seek(s, 0)
    @test deserialize(s) === smbl
    @test deserialize(s) === smbl2
    @test deserialize(s) === smbl3
end

# Module
create_serialization_stream() do s # user-defined module
    mod = b"SomeModule"
    modstring = String(mod)
    eval(Meta.parse("module $(modstring); end"))
    modtype = eval(Meta.parse("$(modstring)"))
    serialize(s, modtype)
    seek(s, 0)
    @test invokelatest(deserialize, s) === modtype
end

# DataType
create_serialization_stream() do s # standard types
    typ1 = UInt8
    serialize(s, typ1)
    typ2 = Bool
    serialize(s, typ2)

    seek(s, 0)
    @test deserialize(s) == typ1
    @test deserialize(s) == typ2
end

create_serialization_stream() do s # user-defined type
    usertype = "SerializeSomeType"
    eval(Meta.parse("abstract type $(usertype) end"))
    utype = eval(Meta.parse("$(usertype)"))
    serialize(s, utype)
    seek(s, 0)
    @test invokelatest(deserialize, s) === utype
end

create_serialization_stream() do s # user-defined type
    usertype = "SerializeSomeType1"
    eval(Meta.parse("mutable struct $(usertype); end"))
    utype = eval(Meta.parse("$(usertype)"))
    serialize(s, utype)
    seek(s, 0)
    @test invokelatest(deserialize, s) === utype
end

create_serialization_stream() do s # user-defined type
    usertype = "SerializeSomeType2"
    eval(Meta.parse("abstract type $(usertype){T} end"))
    utype = eval(Meta.parse("$(usertype)"))
    serialize(s, utype)
    seek(s, 0)
    @test invokelatest(deserialize, s) == utype
end

create_serialization_stream() do s # immutable struct with 1 field
    usertype = "SerializeSomeType3"
    eval(Meta.parse("struct $(usertype){T}; a::T; end"))
    utype = eval(Meta.parse("$(usertype)"))
    serialize(s, utype)
    seek(s, 0)
    @test invokelatest(deserialize, s) == utype
end

create_serialization_stream() do s # immutable struct with 2 fields
    usertype = "SerializeSomeType4"
    eval(Meta.parse("struct $(usertype){T}; a::T; b::T; end"))
    utval = eval(Meta.parse("$(usertype)(1,2)"))
    serialize(s, utval)
    seek(s, 0)
    @test invokelatest(deserialize, s) === utval
end

create_serialization_stream() do s # immutable struct with 3 fields
    usertype = "SerializeSomeType5"
    eval(Meta.parse("struct $(usertype){T}; a::T; b::T; c::T; end"))
    utval = eval(Meta.parse("$(usertype)(1,2,3)"))
    serialize(s, utval)
    seek(s, 0)
    @test invokelatest(deserialize, s) === utval
end

create_serialization_stream() do s # immutable struct with 4 fields
    usertype = "SerializeSomeType6"
    eval(Meta.parse("struct $(usertype){T}; a::T; b::T; c::T; d::T; end"))
    utval = eval(Meta.parse("$(usertype)(1,2,3,4)"))
    serialize(s, utval)
    seek(s, 0)
    @test invokelatest(deserialize, s) === utval
end

create_serialization_stream() do s # union types
    serialize(s, Union{Int,Float64})
    serialize(s, Union{Int,Missing})
    serialize(s, Union{Int,Float64,String})
    serialize(s, [Union{Int,Float64}, Union{String,Symbol}, Union{Int,Missing}])
    seek(s, 0)
    @test deserialize(s) === Union{Int,Float64}
    @test deserialize(s) === Union{Int,Missing}
    @test deserialize(s) === Union{Int,Float64,String}
    @test deserialize(s) == Type[Union{Int,Float64}, Union{String,Symbol}, Union{Int,Missing}]
end

# Expression
create_serialization_stream() do s
    expr = Meta.parse("a = 1")
    serialize(s, expr)

    expr2 = Meta.parse(repeat("a = 1;", 300))
    serialize(s, expr2)

    seek(s, 0)
    @test deserialize(s) == expr
    @test deserialize(s) == expr2
end

# Array
mutable struct TA1
    v::UInt8
end
create_serialization_stream() do s # small 1d array
    arr1 = fill(0x01, 10)    # small 1d array
    serialize(s, arr1)
    arr2 = fill(0x01, 3, 3)  # small 2d array
    serialize(s, arr2)
    arr3 = fill(0x0001, 50)  # large 1d array
    serialize(s, arr3)
    arr4 = reshape([true, false, false, false, true, false, false, false, true], 3, 3)
    serialize(s, arr4)       # boolean array

    arr5 = Vector{TA1}(undef, 3)
    arr5[2] = TA1(0x01)
    serialize(s, arr5)

    seek(s, 0)
    @test deserialize(s) == arr1
    @test deserialize(s) == arr2
    @test deserialize(s) == arr3
    @test deserialize(s) == arr4

    result = deserialize(s)
    @test !isassigned(result,1)
    @test !isassigned(result,3)
    @test result[2].v == arr5[2].v
end

# SubArray
create_serialization_stream() do s # slices
    slc1 = view(UInt8[1,1,1,1], 2:3)
    serialize(s, slc1)
    slc2 = view(repeat(UInt8[1,2,3,4], 1, 4), 1, 2:4)
    serialize(s, slc2)

    seek(s, 0)
    @test deserialize(s) == slc1
    @test deserialize(s) == slc2
end

# Objects that have a SubArray as a type in a type-parameter list
module ArrayWrappers

struct ArrayWrapper{T,N,A<:AbstractArray} <: AbstractArray{T,N}
    data::A
end
ArrayWrapper(data::AbstractArray{T,N}) where {T,N} = ArrayWrapper{T,N,typeof(data)}(data)
Base.size(A::ArrayWrapper) = size(A.data)
Base.size(A::ArrayWrapper, d) = size(A.data, d)
Base.getindex(A::ArrayWrapper, i::Real...) = getindex(A.data, i...)

end

let A = rand(3,4)
    for B in (view(A, :, 2:4), view(A, 2, 1:3))
        C = ArrayWrappers.ArrayWrapper(B)
        io = IOBuffer()
        serialize(io, C)
        seek(io, 0)
        Cd = deserialize(io)
        @test size(Cd) == size(C)
        @test Cd == B
    end
end

# Function
module DefinitelyNotMain
    serialize_test_function() = 1
    serialize_test_function2 = ()->1
end
serialize_test_function = DefinitelyNotMain.serialize_test_function
serialize_test_function2 = DefinitelyNotMain.serialize_test_function2
create_serialization_stream() do s # Base generic function
    serialize(s, sin)
    serialize(s, typeof)
    serialize(s, serialize_test_function)
    serialize(s, serialize_test_function2)

    seek(s, 0)
    @test deserialize(s) === sin
    @test deserialize(s) === typeof
    f1 = deserialize(s)
    f2 = deserialize(s)
    @test first(methods(f1)).module === DefinitelyNotMain
    @test first(methods(f2)).module === DefinitelyNotMain
    @test f1() === 1
    @test f2() === 1
end

# Anonymous Functions
main_ex = quote
    using Serialization
    $create_serialization_stream() do s
        local g() = :magic_token_anon_fun_test
        local gkw(; kw=:thekw) = kw
        serialize(s, g)
        serialize(s, g)
        serialize(s, gkw)

        seekstart(s)
        ds = Serializer(s)
        local g2 = deserialize(ds)
        @test g2 !== g
        $Test.@test Base.invokelatest(g2) === :magic_token_anon_fun_test
        $Test.@test Base.invokelatest(g2) === :magic_token_anon_fun_test
        deserialize(ds) === g2

        local gkw2 = deserialize(s)
        $Test.@test gkw2 !== gkw
        $Test.@test Base.invokelatest(gkw2) === :thekw
        $Test.@test Base.invokelatest(gkw2, kw="kwtest") === "kwtest"

        # issue #21793
        y = x -> (() -> x)
        seekstart(s)
        serialize(s, y)
        seekstart(s)
        y2 = deserialize(s)
        $Test.@test Base.invokelatest() do
            x2 = y2(2)
            x2()
        end === 2
    end
end
# This needs to be run on `Main` since the serializer treats it differently.
Core.eval(Main, main_ex)

# Task
# Runnable tasks drop CodeInstance hints but preserve explicit invoke targets.
serialization_task_result() = 42
create_serialization_stream() do s
    t = Task(serialization_task_result)
    serialize(s, t)
    seek(s, 0)
    r = deserialize(s)
    @test fetch(schedule(r)) === 42
    @test fetch(schedule(t)) === 42
end

serialization_task_invoke() = 43
serialization_task_invoke(x...) = x
function serialization_task_with_invoke(@nospecialize(f), @nospecialize(invoked))
    t = Core._task(f, 0, invoked)
    t.donenotify = Base.ThreadSynchronizer()
    return t
end
for invoked in (Tuple{Vararg}, which(serialization_task_invoke, (Vararg,)))
    create_serialization_stream() do s
        t = serialization_task_with_invoke(serialization_task_invoke, invoked)
        serialize(s, t)
        seek(s, 0)
        r = deserialize(s)
        @test fetch(schedule(r)) === ()
        @test fetch(schedule(t)) === ()
    end
end

create_serialization_stream() do s # user-defined type array
    f = () -> begin task_local_storage(:v, 2); return 1+1 end
    t = Task(f)
    Base.wait(schedule(t))
    serialize(s, t)
    seek(s, 0)
    r = deserialize(s)
    @test r.storage[:v] == 2
    @test r.state === :done
    @test r.exception === nothing
end

struct MyErrorTypeTest <: Exception end
create_serialization_stream() do s # user-defined type array
    t = Task(()->throw(MyErrorTypeTest()))
    @test_throws TaskFailedException(t) Base.wait(schedule(t))
    @test isa(t.exception, MyErrorTypeTest)
    serialize(s, t)
    seek(s, 0)
    r = deserialize(s)
    @test r.state === :failed
end

# corner case: undefined inside immutable struct
struct MyNullable{T}
    hasvalue::Bool
    value::T

    MyNullable{T}() where {T} = new(false)
end
create_serialization_stream() do s
    serialize(s, MyNullable{Any}())
    seekstart(s)
    n = deserialize(s)
    @test isa(n, MyNullable)
    @test !isdefined(n, :value)
end

# cycles
module CycleFoo
    echo(x)=x
    afunc = (x)->x
end
echo(x) = x
create_serialization_stream() do s
    A = Any[1,2,3,abs,abs,CycleFoo.afunc,CycleFoo.afunc,echo,echo,CycleFoo.echo,CycleFoo.echo,4,5]
    A[3] = A
    serialize(s, A)
    seekstart(s)
    b = deserialize(s)
    @test b[3] === b
    @test b[1] == 1
    @test b[4] === abs
    @test b[5] === b[4]
    @test b[5](-1) == 1

    @test b[6] === b[7]
    @test b[6]("Hello") == "Hello"

    @test b[8] === b[9]
    @test b[8]("World") == "World"

    @test b[10] === b[11]
    @test b[10]("foobar") == "foobar"

    @test b[end] == 5
    @test length(b) == length(A)
    @test isa(b,Vector{Any})
end

# shared references
create_serialization_stream() do s
    A = [1,2]
    B = [A,A]
    serialize(s, B)
    seekstart(s)
    C = deserialize(s)
    @test C == B
    @test C[1] === C[2]
end

mutable struct MSingle end
create_serialization_stream() do s
    x = MSingle()
    A = [x, x, MSingle()]
    serialize(s, A)
    seekstart(s)
    C = deserialize(s)
    @test A[1] === x === A[2] !== A[3]
    @test x !== C[1] === C[2] !== C[3]
end

# Regex
create_serialization_stream() do s
    r1 = r"a?b.*"
    serialize(s, r1)
    seekstart(s)
    r2 = deserialize(s)
    @test r1 == r2
end

# issue #13452
module Test13452
using Test, Serialization

module Shell
export foo
foo(x) = error("Instances must implement foo")
end

module Instance1
using ..Shell
Shell.foo(x::Int) = "This is an Int"
end

using .Shell, .Instance1
io = IOBuffer()
serialize(io, foo)
str = String(take!(io))
@test !occursin("Instance1", str)
@test occursin("Shell", str)

end  # module Test13452

# issue #15163
mutable struct B15163{T}
    x::Array{T}
end
let b = IOBuffer()
    serialize(b,B15163([1]))
    seekstart(b)
    c = deserialize(b)
    @test isa(c,B15163) && c.x == [1]
end
# related issue #20066
let b = IOBuffer()
    serialize(b, Dict{Vector, Vector}())
    seekstart(b)
    c = deserialize(b)
    @test isa(c, Dict{Vector, Vector})
end

# issue #15849
let b = IOBuffer()
    vt = Tuple[(1,)]
    serialize(b, vt)
    seekstart(b)
    @test deserialize(b) == vt
end

# issue #1770
let a = ['T', 'e', 's', 't'],
    f = IOBuffer()

    # issue #1770
    serialize(f, a)
    seek(f, 0)
    @test deserialize(f) == a
    f = IOBuffer()
    serialize(f, a)
    seek(f, 0)
    @test deserialize(f) == a

    # issue #4414
    seek(f,0)
    serialize(f, :β)
    seek(f,0)
    @test deserialize(f) === :β
end

# issue #20324
struct T20324{T}
    x::T
end
let x = T20324[T20324(1) for i = 1:2]
    b = IOBuffer()
    serialize(b, x)
    seekstart(b)
    y = deserialize(b)
    @test isa(y,Vector{T20324})
    @test y == x
end

@testset "serializer header" begin
    io = IOBuffer()
    serialize(io, ())
    seekstart(io)
    b = read(io)
    @test b[1] == Serialization.HEADER_TAG
    @test b[2:3] == b"JL"
    @test b[4] == Serialization.ser_version
    @test (b[5] & 0x3) == (ENDIAN_BOM == 0x01020304)
    @test ((b[5] & 0xc)>>2) == (sizeof(Int) == 8)
    @test (b[5] & 0xf0) == 0
    @test all(b[6:8] .== 0)

    # Detection of incompatible binary serializations
    function corrupt_header(bytes, offset, val)
        b = copy(bytes)
        b[offset] = val
        IOBuffer(b)
    end
    @test_throws(
        ErrorException("""Cannot read stream serialized with a newer version of Julia.
                          Got data version 255 > current version $(Serialization.ser_version)"""),
        deserialize(corrupt_header(b, 4, 0xff)))
    @test_throws(ErrorException("Unknown word size flag in header"),
                 deserialize(corrupt_header(b, 5, 2<<2)))
    @test_throws(ErrorException("Unknown endianness flag in header"),
                 deserialize(corrupt_header(b, 5, 2)))
    other_wordsize = sizeof(Int) == 8 ? 4 : 8
    other_endianness = bswap(ENDIAN_BOM)
    @test_throws(ErrorException("Serialized byte order mismatch ($(repr(other_endianness)))"),
                 deserialize(corrupt_header(b, 5, UInt8(ENDIAN_BOM != 0x01020304))))
end

# issue #26979
let io = IOBuffer()
    function gen_f(a::T) where T
        f = x -> T(x)
        return f
    end
    f = gen_f(1f0)
    serialize(io, f)
    seekstart(io)
    f2 = deserialize(io)
    @test invokelatest(f2, 1) === 1f0
end

# using a filename; #30151
let f = tempname(), x = [rand(2,2), :x, "hello"]
    serialize(f, x)
    @test deserialize(f) == x
    rm(f)
end

let f_data
    # a serialized function from v1.0/v1.1
    if Int === Int64
        f_data = "N0pMBwQAAAA0MxMAAAAAAAAAAAEFIyM1IzYiAAAAABBYH04BBE1haW6bRCIAAAAAIgAAAABNTEy+AQIjNRUAI+AjAQAAAAAAAAAfTgEETWFpbkQBAiM1AQdSRVBMWzNdvxBTH04BBE1haW6bRAMAAAAzLAAARkYiAAAAAE7BTBsVRuIWA1YkH04BBE1haW5EAQEq4SXhFgNWJB9OAQRNYWluRJ0o4CXiFgFVKOEVAAbiAQAAAAEAAAABAAAATuIVRuA0EAEMTGluZUluZm9Ob2RlH04BBE1haW6bRB9OAQRNYWluRAECIzUBB1JFUExbM13g3xXfFeIAAAAVRuKifX5MTExMTuIp"
    else
        f_data = "N0pMBwAAAAA0MxMAAAAAAAAAAAEFIyM1IzYiAAAAABBYH04BBE1haW6bRCIAAAAAIgAAAABNTEy+AQIjNRUAI78jAQAAAAAAAAAfTgEETWFpbkQBAiM1AQdSRVBMWzJdvxBTH04BBE1haW6bRAMAAAAzLAAARkYiAAAAAE7BTBsVRsEWA1YkH04BBE1haW5EAQEqwCXAFgNWJB9OAQRNYWluRJ0ovyXBFgFVKMAVAAbBAQAAAAEAAAABAAAATsEVRr80EAEMTGluZUluZm9Ob2RlH04BBE1haW6bRB9OAQRNYWluRAECIzUBB1JFUExbMl2/vhW+FcEAAAAVRsGifX5MTExMTsEp"
    end
    f = deserialize(IOBuffer(base64decode(f_data)))
    @test invokelatest(f, 10,3) == 23
end

# issue #33466, IdDict
let d = IdDict([1] => 2, [3] => 4), io = IOBuffer()
    serialize(io, d)
    seekstart(io)
    ds = deserialize(io)
    @test Dict(d) == Dict(ds)
    @test all([k in keys(ds) for k in keys(ds)])
end

# issue #35030, shared references to Strings
let s = join(rand('a':'z', 1024)), io = IOBuffer()
    serialize(io, (s, s))
    seekstart(io)
    s2 = deserialize(io)
    @test Base.summarysize(s2) < 2*sizeof(s)
end

# issue #39895
@eval Main begin
    using Test, Serialization
    let g = gensym(:g)
        closure = eval(:(f -> $g(x) = f(x)))
        inc(x) = x + 1
        b = IOBuffer()
        serialize(b, closure(inc))
        seekstart(b)
        f = deserialize(b)
        # this should not crash
        @test_broken f(1) == 2
    end
end

let c1 = Threads.Condition()
    c2 = Threads.Condition(c1.lock)
    lock(c2)
    t = @task nothing
    Base.schedule_on_notify!(c1, t)
    c3, c4 = deserialize(IOBuffer(sprint(serialize, [c1, c2])))::Vector{Threads.Condition}
    @test c3.lock === c4.lock
    @test islocked(c1)
    @test !islocked(c3)
    @test !isempty(c1.waitq)
    @test isempty(c2.waitq)
    @test isempty(c3.waitq)
    @test isempty(c4.waitq)
    notify(c1)
    unlock(c2)
    wait(t)
end

@testset "LazyString" begin
    l1 = lazy"a $1 b $2"
    l2 = deserialize(IOBuffer(sprint(serialize, l1)))
    @test l2.str === l1.str
    @test l2 == l1
    @test l2.parts === ()
end

@testset "Core.IntrinsicFunction" begin
    create_serialization_stream() do s
        serialize(s, Core.Intrinsics.add_int)
        serialize(s, Core.Intrinsics.xor_int)
        serialize(s, Core.Intrinsics.sub_float)
        seekstart(s)
        @test deserialize(s) === Core.Intrinsics.add_int
        @test deserialize(s) === Core.Intrinsics.xor_int
        @test deserialize(s) === Core.Intrinsics.sub_float
    end
end

@testset "Docstrings" begin
    undoc = Docs.undocumented_names(Serialization)
    @test_broken isempty(undoc)
    @test undoc == [:AbstractSerializer, :Serializer]
end

# test method definitions from v1.11
if Int === Int64
    let f_data = "N0pMGgQAAAAWAQEFdGh1bmsbFUbnFgEBBXRodW5rGxVG4DoWAQEGbWV0aG9kAQtmMTExX3RvXzExMhUABuABAAAA4BUAB+AAAAAAThVG4DQQAQxMaW5lSW5mb05vZGUfTptEH04BBE1haW5EAQ90b3AtbGV2ZWwgc2NvcGUBBG5vbmW+vhUAAd8V305GTk4JAQAAAAAAAAAJ//////////9MTExMAwADAAUAAAX//xYBAQZtZXRob2QsBwAWAlYkH06bRAEGVHlwZW9mLAcAFgNWJB9Om0QBBHN2ZWMo4iQfTptETxYBViQfTptEAQRzdmVjFgRWJB9Om0QBBHN2ZWMo4yjkGhfgAQRub25lFgMBBm1ldGhvZCwHACjlGxVG5AEBXhYDViQfTptElyQfTp5EAQNWYWzhFgFWKOEWBFYkH06eRAELbGl0ZXJhbF9wb3co4CXhKOI6KOMVAAbkAQAAAAEAAAABAAAAAQAAAAAAAADkFQAH5AAAAAAAAAAAAAAAAAAAAAAAAAAAThVG4DQsCwAfTgEETWFpbkQBBG5vbmUBBG5vbmW/vhUAAeGifRXhAAhORk5OCQEAAAAAAAAACf//////////TExMTAMAAwAFAAAF//86ThUABucBAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAAAAAOcVAAfnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOFUbgNCwLAB9OAQRNYWluRCwNAAEEbm9uZb6+FQAB3xXfTkZOTgkBAAAAAAAAAAn//////////0xMTEwDAAMABQAABf//"
        @eval Main function f111_to_112 end
        Core.eval(Main, with(Serialization.current_module => Main) do
                 deserialize(IOBuffer(base64decode(f_data)))
             end)
        @test @invokelatest(Main.f111_to_112(16)) == 256
    end
end

@testset "MemoryRef" begin
    old_m = Memory{Int}(undef, 10)
    for i in 1:10
        old_m[i] = i^2
    end
    # Test roundtrip at every offset
    for idx in 1:10
        old_x = memoryref(old_m, idx)
        @test old_x[] == idx^2
        old_d = Dict(:x => old_x)

        old_str = sprint(serialize, old_d)
        new_d = deserialize(IOBuffer(old_str))

        @test new_d[:x] isa MemoryRef
        @test new_d[:x][] == idx^2
    end
end

@testset "Memory" begin
    old_m = Memory{Int}(undef, 10)
    for i in 1:10
        old_m[i] = i^3
    end
    @test old_m[5] == 125
    old_d = Dict(:m => old_m)

    old_str = sprint(serialize, old_d)
    new_d = deserialize(IOBuffer(old_str))

    @test new_d[:m] isa Memory
    @test new_d[:m][5] == 125
end

@testset "CancellationTokenSource" begin
    using Base: cancel!, CancellationToken, CancellationTokenSource
    # a diamond: identity sharing of parents must survive the round-trip
    root = CancellationTokenSource()
    left = CancellationTokenSource(CancellationToken(root))
    right = CancellationTokenSource(CancellationToken(root))
    child = CancellationTokenSource(CancellationToken(left), CancellationToken(right))
    buf = IOBuffer()
    serialize(buf, (root, left, right, child))
    seekstart(buf)
    r2, l2, rt2, c2 = deserialize(buf)
    @test r2 isa CancellationTokenSource && c2.nparents == 2
    @test Base._cancel_parent(c2, 1) === l2 && Base._cancel_parent(c2, 2) === rt2
    @test Base._cancel_parent(l2, 1) === r2 && Base._cancel_parent(rt2, 1) === r2
    @test !Base.iscancelled(c2)
    # the deserialized graph is relinked: cancelling its root reaches the leaf
    cancel!(r2)
    @test Base.iscancelled(c2) && Base.iscancelled(l2) && Base.iscancelled(rt2)
    # ...without affecting the original graph
    @test !Base.iscancelled(root) && !Base.iscancelled(child)

    # cancellation state round-trips
    src = CancellationTokenSource()
    cancel!(src, Base.CANCEL_REQUEST_ABANDON_EXTERNAL)
    buf = IOBuffer()
    serialize(buf, src)
    seekstart(buf)
    s2 = deserialize(buf)
    @test Base.cancel_severity(s2) === Base.CANCEL_REQUEST_ABANDON_EXTERNAL

    # the copy is independent: cancelling the original after serialization
    # does not affect the deserialized graph
    p = CancellationTokenSource()
    c = CancellationTokenSource(CancellationToken(p))
    buf = IOBuffer()
    serialize(buf, (p, c))
    seekstart(buf)
    cancel!(p) # does not affect the serialized bytes
    p3, c3 = deserialize(buf)
    @test !Base.iscancelled(c3) # p3 was not cancelled at relink time

    # deserializing under an already-cancelled parent is born cancelled:
    # a stream may legally record a cancelled parent with an uncancelled
    # child (e.g. the serializer raced an in-flight cancel! walk); the
    # child inherits the state when it relinks under the parent
    p = CancellationTokenSource()
    c = CancellationTokenSource(CancellationToken(p))
    Base._raise_state!(p, 0x01) # cancel p without walking to c
    @test !Base.iscancelled(c)
    buf = IOBuffer()
    serialize(buf, c) # parents serialize first, with their state
    seekstart(buf)
    c4 = deserialize(buf)
    @test Base.iscancelled(Base._cancel_parent(c4, 1))
    @test Base.iscancelled(c4)

    # a corrupt stream cannot inject an out-of-range severity
    src = CancellationTokenSource()
    buf = IOBuffer()
    serialize(buf, src)
    data = take!(buf)
    data[end] = 0xbf # state byte: invalid severity 0xbf
    @test_throws ArgumentError deserialize(IOBuffer(data))
end

@testset "WaitEntryN" begin
    # variable-sized runtime object: it must be reconstructed through the
    # runtime allocator (the generic path would allocate only the fixed
    # datatype size, and the GC would then scan a nonexistent slot tail)
    w = Base.WaitEntryN(current_task(), 4)
    buf = IOBuffer()
    serialize(buf, w)
    seekstart(buf)
    w2 = deserialize(buf)
    @test w2 isa Core.WaitEntryN
    @test Base._nslots(w2) == 4
    # transient wait state does not round-trip: no task, fresh free slots
    @test (@atomic :monotonic w2.task) === nothing
    @test all(i -> Base._slot_owner(w2, i) === nothing, 1:4)
    GC.gc(true)
    # identity sharing within one stream
    buf = IOBuffer()
    serialize(buf, (w, w))
    seekstart(buf)
    a, b = deserialize(buf)
    @test a === b && Base._nslots(a) == 4
    GC.gc(true)
end
