--
-- BASIC.LUA           Copyright (c) 2007-08, Asko Kauppi <akauppi@gmail.com>
--
-- Selftests for Lua Lanes
--
-- To do:
--      - ...
--
local config = { with_timers = false, strip_functions = false, internal_allocator = "libc"}
-- calling configure more than once should work (additional called are ignored)
local require_lanes_result_1, require_lanes_result_2 = require "lanes".configure(config).configure()
print("require_lanes_result:", require_lanes_result_1, require_lanes_result_2)
local lanes = require_lanes_result_1

local require_assert_result_1, require_assert_result_2 = require "assert"    -- assert.fails()
print("require_assert_result:", require_assert_result_1, require_assert_result_2)

local lanes_gen     assert(lanes.gen)
local lanes_linda = assert(lanes.linda)

local tostring=     assert(tostring)

local function PRINT(...)
    local str=""
    for i=1,select('#',...) do
        str= str..tostring(select(i,...)).."\t"
    end
    if io then
        io.stderr:write(str.."\n")
    end
end

local SLEEP = function(...)
    local k, v = lanes.sleep(...)
    assert(k == nil and v == "timeout")
end

local gc_cb = function(name_, status_)
    PRINT("				---> lane '" .. name_ .. "' collected with status '" .. status_ .. "'")
end
--gc_cb = nil


---=== Local helpers ===---

local tables_match

-- true if 'a' is a subtable of 'b'
--
local function subtable(a, b)
    --
    assert(type(a)=="table" and type(b)=="table")

    for k,v in pairs(b) do
        if type(v)~=type(a[k]) then
            return false    -- not subtable (different types, or missing key)
        elseif type(v)=="table" then
            if not tables_match(v,a[k]) then return false end
        else
            if a[k] ~= v then return false end
        end
    end
    return true     -- is a subtable
end

-- true when contents of 'a' and 'b' are identical
--
tables_match= function(a, b)
    return subtable(a, b) and subtable(b, a)
end

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Tasking (basic) ===---", "\n\n")

local function task(a, b, c)
    lane_threadname("task("..a..","..b..","..c..")")
    --error "111"     -- testing error messages
    assert(hey)
    local v=0
    for i=a,b,c do
        v= v+i
    end
    return v, hey
end

local task_launch= lanes.gen("", { name = 'auto', globals={hey=true}, gc_cb = gc_cb }, task)
    -- base stdlibs, normal priority

-- 'task_launch' is a factory of multithreaded tasks, we can launch several:

local lane1= task_launch(100,200,3)
assert.fails(function() print(lane1[lane1]) end) -- indexing the lane with anything other than a string or a number should fail

local lane2= task_launch(200,300,4)

-- At this stage, states may be "pending", "running" or "done"

local st1,st2= lane1.status, lane2.status
PRINT(st1,st2)
assert(st1=="pending" or st1=="running" or st1=="done")
assert(st2=="pending" or st2=="running" or st2=="done")

-- Accessing results ([1..N]) pends until they are available
--
PRINT("waiting...")
local v1, v1_hey= lane1[1], lane1[2]
local v2, v2_hey= lane2[1], lane2[2]

PRINT(v1, v1_hey)
assert(v1_hey == true)

PRINT(v2, v2_hey)
assert(v2_hey == true)

assert(lane1.status == "done")
assert(lane1.status == "done")
lane1, lane2 = nil
collectgarbage()

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Tasking (cancelling) ===---", "\n\n")

local task_launch2= lanes.gen("", { name = 'auto', globals={hey=true}, gc_cb = gc_cb }, task)

local N=999999999
local lane9= task_launch2(1,N,1)   -- huuuuuuge...

-- Wait until state changes "pending"->"running"
--
local st
local t0= os.time()
while os.time()-t0 < 5 do
    st= lane9.status
    io.stderr:write((i==1) and st.." " or '.')
    if st~="pending" then break end
end
PRINT(" "..st)

if st=="error" then
    local _= lane9[0]  -- propagate the error here
end
if st=="done" then
    error("Looping to "..N.." was not long enough (cannot test cancellation)")
end
assert(st=="running")

-- when running under luajit, the function is JIT-ed, and the instruction count isn't hit, so we need a different hook
lane9:cancel(jit and "line" or "count", 100) -- 0 timeout, hook triggers cancelslation when reaching the specified count

local t0= os.time()
while os.time()-t0 < 5 do
    st= lane9.status
    io.stderr:write((i==1) and st.." " or '.')
    if st~="running" then break end
end
PRINT(" "..st)
assert(st == "cancelled", "st is '" .. st .. "' instead of 'cancelled'")

-- cancellation of lanes waiting on a linda
local limited = lanes_linda{name = "limited"}
assert.fails(function() limited:limit("key", -1) end)
assert.failsnot(function() limited:limit("key", 1) end)
-- [[################################################
limited:send("key", "hello") -- saturate linda
for k, v in pairs(limited:dump()) do
    PRINT("limited[" .. tostring(k) .. "] = " .. tostring(v))
end
local wait_send = function()
    local a,b
    set_finalizer(function(err, stack_tbl) print("wait_send", a, b, " -> ", tostring(err)) end)
    print("in wait_send")
    a,b = limited:send("key", "bybye") -- infinite timeout, returns only when lane is cancelled
end

local wait_send_lane = lanes.gen("*", { name = 'auto' }, wait_send)()
repeat
    io.stderr:write('!')
    -- currently mingw64 builds can deadlock if we cancel the lane too early (before the linda blocks, at it causes the linda condvar not to be signalled)
    lanes.sleep(0.1)
until wait_send_lane.status == "waiting"
PRINT "wait_send_lane is waiting"
wait_send_lane:cancel() -- hard cancel, 0 timeout
repeat until wait_send_lane.status == "cancelled"
PRINT "wait_send_lane is cancelled"
--################################################]]
local wait_receive = function()
    local k, v
    set_finalizer(function(err, stack_tbl) print("wait_receive", k, v, " -> ", tostring(err)) end)
    k, v = limited:receive("dummy") -- infinite timeout, returns only when lane is cancelled
end

local wait_receive_lane = lanes.gen("*", { name = 'auto' }, wait_receive)()
repeat
    io.stderr:write('!')
    -- currently mingw64 builds can deadlock if we cancel the lane too early (before the linda blocks, at it causes the linda condvar not to be signalled)
    lanes.sleep(0.1)
until wait_receive_lane.status == "waiting"
PRINT "wait_receive_lane is waiting"
wait_receive_lane:cancel() -- hard cancel, 0 timeout
repeat until wait_receive_lane.status == "cancelled"
PRINT "wait_receive_lane is cancelled"
--################################################]]
local wait_receive_batched = function()
    local k, v1, v2
    set_finalizer(function() print("wait_receive_batched", k, v1, v2) end)
    k, v1, v2 = limited:receive_batched("dummy", 2) -- infinite timeout, returns only when lane is cancelled
end

local wait_receive_batched_lane = lanes.gen("*", { name = 'auto' }, wait_receive_batched)()
repeat until wait_receive_batched_lane.status == "waiting"
PRINT "wait_receive_batched_lane is waiting"
wait_receive_batched_lane:cancel() -- hard cancel, 0 timeout
repeat until wait_receive_batched_lane.status == "cancelled"
PRINT "wait_receive_batched_lane is cancelled"
--################################################]]

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Communications ===---", "\n\n")

local function WR(...) io.stderr:write(...) end

local chunk= function(linda)
    local function receive() return linda:receive("->") end
    local function send(...) local _res, _err = linda:send("<-", ...) assert(_res == true and _err == nil) end

    WR("chunk ", "Lane starts!\n")

    local k,v
    k,v=receive(); WR("chunk ", v.." received (expecting 1)\n"); assert(k and v==1)
    k,v=receive(); WR("chunk ", v.." received (expecting 2)\n"); assert(k and v==2)
    k,v=receive(); WR("chunk ", v.." received  (expecting 3)\n"); assert(k and v==3)
    k,v=receive(); WR("chunk ", tostring(v).." received  (expecting nil from __lanesconvert)\n"); assert(k and v==nil, "table with __lanesconvert==lanes.null should be received as nil, got " .. tostring(v)) -- a table with __lanesconvert was sent
    k,v=receive(); WR("chunk ", tostring(v).." received (expecting nil)\n"); assert(k and v==nil)

    send(4,5,6);              WR("chunk ", "4,5,6 sent\n")
    send 'aaa';               WR("chunk ", "'aaa' sent\n")
    send(nil);                WR("chunk ", "nil sent\n")
    send { 'a', 'b', 'c', d=10 }; WR("chunk ","{'a','b','c',d=10} sent\n")

    k,v=receive(); WR("chunk ", v.." received\n"); assert(v==4)

    local subT1 = { "subT1"}
    local subT2 = { "subT2"}
    send { subT1, subT2, subT1, subT2}; WR("chunk ", "{ subT1, subT2, subT1, subT2} sent\n")

    WR("chunk ", "Lane ends!\n")
end

local linda = lanes_linda{name = "communications"}
assert(type(linda) == "userdata" and tostring(linda) == "Linda: communications")
    --
    -- ["->"] master -> slave
    -- ["<-"] slave <- master

WR "test linda:get/set..."
linda:set("<->", "x", "y", "z")
local b,x,y,z = linda:get("<->", 1)
assert(b == 1 and x == "x" and y == nil and z == nil)
local b,x,y,z = linda:get("<->", 2)
assert(b == 2 and x == "x" and y == "y" and z == nil)
local b,x,y,z = linda:get("<->", 3)
assert(b == 3 and x == "x" and y == "y" and z == "z")
local b,x,y,z,w = linda:get("<->", 4)
assert(b == 3 and x == "x" and y == "y" and z == "z" and w == nil)
local k, x = linda:receive("<->")
assert(k == "<->" and x == "x")
local k,y,z = linda:receive_batched("<->", 2)
assert(k == "<->" and y == "y" and z == "z")
linda:set("<->")
local b,x,y,z,w = linda:get("<->", 4)
assert(b == 0 and x == nil and y == nil and z == nil and w == nil)
WR "ok\n"

local function PEEK(...) return linda:get("<-", ...) end
local function SEND(...) local _res, _err = linda:send("->", ...) assert(_res == true and _err == nil) end
local function RECEIVE() local k,v = linda:receive(1, "<-") return v end

local comms_lane = lanes.gen("io", { name = 'auto', gc_cb = gc_cb }, chunk)(linda)     -- prepare & launch

SEND(1);  WR("main ", "1 sent\n")
SEND(2);  WR("main ", "2 sent\n")
SEND(3);  WR("main ", "3 sent\n")
SEND(setmetatable({"should be ignored"},{__lanesconvert=lanes.null})); WR("main ", "__lanesconvert table sent\n")
for i=1,40 do
    WR "."
    SLEEP(0.0001)
    assert(PEEK() == 0)    -- nothing coming in, yet
end
SEND(nil);  WR("\nmain ", "nil sent\n")

local a,b,c = RECEIVE(), RECEIVE(), RECEIVE()

print("lane status: " .. comms_lane.status)
if comms_lane.status == "error" then
    print(comms_lane:join())
else
    WR("main ", tostring(a)..", "..tostring(b)..", "..tostring(c).." received\n")
end

assert(a==4 and b==5 and c==6)

local aaa = RECEIVE(); WR("main ", aaa.." received\n")
assert(aaa=='aaa')

local null = RECEIVE();   WR(tostring(null).." received\n")
assert(null==nil)

local out_t = RECEIVE();   WR(type(out_t).." received\n")
assert(tables_match(out_t, {'a','b','c',d=10}))

assert(PEEK() == 0)
SEND(4)

local complex_table = RECEIVE(); WR(type(complex_table).." received\n")
assert(complex_table[1] == complex_table[3] and complex_table[2] == complex_table[4])
WR(table.concat({complex_table[1][1],complex_table[2][1],complex_table[3][1],complex_table[4][1]},", "))

WR("collectgarbage")
comms_lane = nil
collectgarbage()
-- wait
WR("waiting 1s")
SLEEP(1)

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Stdlib naming ===---", "\n\n")

local function dump_g(_x)
    lane_threadname "dump_g"
    assert(print)
    print("### dumping _G for '" .. _x .. "'")
    for k, v in pairs(_G) do
        print("\t" .. k .. ": " .. type(v))
    end
    return true
end

local function io_os_f(_x)
    lane_threadname "io_os_f"
    assert(print)
    print("### checking io and os libs existence for '" .. _x .. "'")
    assert(io)
    assert(os)
    return true
end

local function coro_f(_x)
    lane_threadname "coro_f"
    assert(print)
    print("### checking coroutine lib existence for '" .. _x .. "'")
    assert(coroutine)
    return true
end

assert.fails(function() lanes_gen("xxx", {name = 'auto', gc_cb = gc_cb }, io_os_f) end)

local stdlib_naming_tests =
{
    -- { "", dump_g},
    -- { "coroutine", dump_g},
    -- { "io", dump_g},
    -- { "bit32", dump_g},
    { "coroutine?", coro_f}, -- in Lua 5.1, the coroutine base library doesn't exist (coroutine table is created when 'base' is opened)
    { "*", io_os_f},
    { "io,os", io_os_f},
    { "io+os", io_os_f},
    { "io,os,base", io_os_f},
}

for _, t in ipairs(stdlib_naming_tests) do
    local f= lanes.gen(t[1], { name = 'auto', gc_cb = gc_cb }, t[2])     -- any delimiter will do
    assert(f(t[1])[1])
end

WR("collectgarbage")
collectgarbage()

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Comms criss cross ===---", "\n\n")

-- We make two identical lanes, which are using the same Linda channel.
--
local tc = lanes.gen("io", { name = 'auto', gc_cb = gc_cb },
  function(linda, ch_in, ch_out)
        lane_threadname("criss cross " .. ch_in .. " -> " .. ch_out)
    local function STAGE(str)
        io.stderr:write(ch_in..": "..str.."\n")
        linda:send(nil, ch_out, str)
        local k,v= linda:receive(nil, ch_in)
        assert(v==str)
    end
    STAGE("Hello")
    STAGE("I was here first!")
    STAGE("So what?")
  end
)

local linda= lanes_linda{name = "criss cross"}

local a,b= tc(linda, "A","B"), tc(linda, "B","A")   -- launching two lanes, twisted comms

local _= a[1],b[1]  -- waits until they are both ready

WR("collectgarbage")
a, b = nil
collectgarbage()

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== Receive & send of code ===---", "\n\n")

local upvalue="123"

local function chunk2(linda)
    assert(upvalue=="123")    -- even when running as separate thread
    -- function name & line number should be there even as separate thread
    --
    local info= debug.getinfo(1)    -- 1 = us
    --
    PRINT("linda named-> '" ..tostring(linda).."'")
    PRINT "debug.getinfo->"
    for k,v in pairs(info) do PRINT(k,v) end

    -- some assertions are adjusted depending on config.strip_functions, because it changes what we get out of debug.getinfo
    assert(info.nups == (_VERSION == "Lua 5.1" and 4 or 5), "bad nups " .. info.nups)    -- upvalue + config + PRINT + tostring + _ENV (Lua > 5.2 only)
    assert(info.what == "Lua", "bad what")
    --assert(info.name == "chunk2")   -- name does not seem to come through
    assert(config.strip_functions and info.source=="=?" or string.match(info.source, "^@.*basic.lua$"), "bad info.source")
    assert(config.strip_functions and info.short_src=="?" or string.match(info.short_src, "^.*basic.lua$"), "bad info.short_src")
    -- These vary so let's not be picky (they're there..)
    --
    assert(info.linedefined == 431, "bad linedefined")   -- start of 'chunk2'
    assert(config.strip_functions and info.currentline==-1 or info.currentline > info.linedefined, "bad currentline")   -- line of 'debug.getinfo'
    assert(info.lastlinedefined > info.currentline, "bad lastlinedefined")   -- end of 'chunk2'
    local k,func= linda:receive("down")
    assert(type(func)=="function", "not a function")
    assert(k=="down")

    func(linda)

    local k,str= linda:receive("down")
    assert(str=="ok", "bad receive result")

    linda:send("up", function() return ":)" end, "ok2")
end

local linda = lanes_linda{name = "auto"}
local t2 = lanes.gen("debug,string,io", { name = 'auto', gc_cb = gc_cb }, chunk2)(linda)     -- prepare & launch
linda:send("down", function(linda) linda:send("up", "ready!") end,
                    "ok")
-- wait to see if the tiny function gets executed
--
local k,s= linda:receive(1, "up")
if t2.status == "error" then
    PRINT("t2 error: " , t2:join())
end
PRINT(s)
assert(s=="ready!")

-- returns of the 'chunk2' itself
--
local k,f= linda:receive("up")
assert(type(f)=="function")

local s2= f()
assert(s2==":)")

local k,ok2= linda:receive("up")
assert(ok2 == "ok2")

-- ##################################################################################################
-- ##################################################################################################
-- ##################################################################################################

PRINT("\n\n", "---=== :join test ===---", "\n\n")

-- NOTE: 'unpack()' cannot be used on the lane handle; it will always return nil
--       (unless [1..n] has been read earlier, in which case it would seemingly
--       work).

local S = lanes.gen("table", { name = 'auto', gc_cb = gc_cb },
  function(arg)
        lane_threadname "join test lane"
        set_finalizer(function() end)
    aux= {}
    for i, v in ipairs(arg) do
       table.insert (aux, 1, v)
    end
        -- unpack was renamed table.unpack in Lua 5.2: cater for both!
    return (unpack or table.unpack)(aux)
end)

h = S { 12, 13, 14 }     -- execution starts, h[1..3] will get the return values
-- wait a bit so that the lane has a chance to set its debug name
SLEEP(0.5)
print("joining with '" .. h:get_threadname() .. "'")
local r,a,b,c,d= h:join()
if h.status == "error" then
    print(h:get_threadname(), "error: " , r, a, b, c, d)
else
    print(h:get_threadname(), r,a,b,c,d)
    assert(r == true)
    assert(a == 14)
    assert(b == 13)
    assert(c == 12)
    assert(d == nil)
end

local nameof_type, nameof_name = lanes.nameof(print)
PRINT("name of " .. nameof_type .. " print = '" .. nameof_name .. "'")
-- install a finalizer that gets called upon Lanes's internal Universe is GCed.
-- that way, we print our message after anything that can be output by lanes that are still running at that point
lanes.finally(function() io.stderr:write "\n=======================================\nTEST OK\n" end)
