# vim:set ft= ts=4 sw=4 et fdm=marker:

use Test::Nginx::Socket::Lua;

repeat_each(2);

plan tests => repeat_each() * (blocks() * 3 + 5);

$ENV{TEST_NGINX_RESOLVER} ||= '8.8.8.8';

our $StapScript = <<'_EOC_';
global ids, cur

function gen_id(k) {
    if (ids[k]) return ids[k]
    ids[k] = ++cur
    return cur
}

F(ngx_http_handler) {
    delete ids
    cur = 0
}

/*
F(ngx_http_lua_run_thread) {
    id = gen_id($ctx->cur_co)
    printf("run thread %d\n", id)
}

probe process("/usr/local/openresty-debug/luajit/lib/libluajit-5.1.so.2").function("lua_resume") {
    id = gen_id($L)
    printf("lua resume %d\n", id)
}
*/

M(http-lua-user-coroutine-resume) {
    p = gen_id($arg2)
    c = gen_id($arg3)
    printf("resume %x in %x\n", c, p)
}

M(http-lua-thread-yield) {
    println("thread yield")
}

/*
F(ngx_http_lua_coroutine_yield) {
    printf("yield %x\n", gen_id($L))
}
*/

M(http-lua-user-coroutine-yield) {
    p = gen_id($arg2)
    c = gen_id($arg3)
    printf("yield %x in %x\n", c, p)
}

F(ngx_http_lua_atpanic) {
    printf("lua atpanic(%d):", gen_id($L))
    print_ubacktrace();
}

M(http-lua-user-coroutine-create) {
    p = gen_id($arg2)
    c = gen_id($arg3)
    printf("create %x in %x\n", c, p)
}

F(ngx_http_lua_ngx_exec) { println("exec") }

F(ngx_http_lua_ngx_exit) { println("exit") }
F(ngx_http_lua_ffi_exit) { println("exit") }
_EOC_

no_shuffle();
no_long_string();
run_tests();

__DATA__

=== TEST 1: basic coroutine print
--- config
    location /lua {
        content_by_lua '
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield

            function f()
                local cnt = 0
                for i = 1, 20 do
                    ngx.say("Hello, ", cnt)
                    cy()
                    cnt = cnt + 1
                end
            end

            local c = cc(f)
            for i=1,3 do
                cr(c)
                ngx.say("***")
            end
        ';
    }
--- request
GET /lua
--- stap2 eval: $::StapScript
--- response_body
Hello, 0
***
Hello, 1
***
Hello, 2
***
--- no_error_log
[error]



=== TEST 2: basic coroutine2
--- config
    location /lua {
        content_by_lua '
            function f(fid)
                local cnt = 0
                while true do
                    ngx.say("cc", fid, ": ", cnt)
                    coroutine.yield()
                    cnt = cnt + 1
                end
            end

            local ccs = {}
            for i=1,3 do
                ccs[#ccs+1] = coroutine.create(function() f(i) end)
            end

            for i=1,9 do
                local cc = table.remove(ccs, 1)
                coroutine.resume(cc)
                ccs[#ccs+1] = cc
            end
        ';
    }
--- request
GET /lua
--- response_body
cc1: 0
cc2: 0
cc3: 0
cc1: 1
cc2: 1
cc3: 1
cc1: 2
cc2: 2
cc3: 2
--- no_error_log
[error]



=== TEST 3: basic coroutine and cosocket
--- config
    resolver $TEST_NGINX_RESOLVER ipv6=off;
    location /lua {
        content_by_lua '
            function worker(url)
                local sock = ngx.socket.tcp()
                local ok, err = sock:connect(url, 80)
                coroutine.yield()
                if not ok then
                    ngx.say("failed to connect to: ", url, " error: ", err)
                    return
                end
                coroutine.yield()
                ngx.say("successfully connected to: ", url)
                sock:close()
            end

            local urls = {
                "agentzh.org",
                "iscribblet.org",
                "openresty.org"
            }

            local ccs = {}
            for i, url in ipairs(urls) do
                local cc = coroutine.create(function() worker(url) end)
                ccs[#ccs+1] = cc
            end

            while true do
                if #ccs == 0 then break end
                local cc = table.remove(ccs, 1)
                local ok = coroutine.resume(cc)
                if ok then
                    ccs[#ccs+1] = cc
                end
            end

            ngx.say("*** All Done ***")
        ';
    }
--- request
GET /lua
--- response_body
successfully connected to: agentzh.org
successfully connected to: iscribblet.org
successfully connected to: openresty.org
*** All Done ***
--- no_error_log
[error]
--- timeout: 10



=== TEST 4: coroutine.wrap(generate prime numbers)
--- config
    location /lua {
        content_by_lua '
            -- generate all the numbers from 2 to n
            function gen (n)
              return coroutine.wrap(function ()
                for i=2,n do coroutine.yield(i) end
              end)
            end

            -- filter the numbers generated by g, removing multiples of p
            function filter (p, g)
              return coroutine.wrap(function ()
                while 1 do
                  local n = g()
                  if n == nil then return end
                  if math.fmod(n, p) ~= 0 then coroutine.yield(n) end
                end
              end)
            end

            N = 10
            x = gen(N)		-- generate primes up to N
            while 1 do
              local n = x()		-- pick a number until done
              if n == nil then break end
              ngx.say(n)		-- must be a prime number
              x = filter(n, x)	-- now remove its multiples
            end
        ';
    }
--- request
GET /lua
--- response_body
2
3
5
7
--- no_error_log
[error]



=== TEST 5: coroutine.wrap(generate prime numbers,reset create and resume)
--- config
    location /lua {
        content_by_lua '
            coroutine.create = nil
            coroutine.resume = nil
            -- generate all the numbers from 2 to n
            function gen (n)
              return coroutine.wrap(function ()
                for i=2,n do coroutine.yield(i) end
              end)
            end

            -- filter the numbers generated by g, removing multiples of p
            function filter (p, g)
              return coroutine.wrap(function ()
                while 1 do
                  local n = g()
                  if n == nil then return end
                  if math.fmod(n, p) ~= 0 then coroutine.yield(n) end
                end
              end)
            end

            N = 10 
            x = gen(N)		-- generate primes up to N
            while 1 do
              local n = x()		-- pick a number until done
              if n == nil then break end
              ngx.say(n)		-- must be a prime number
              x = filter(n, x)	-- now remove its multiples
            end
        ';
    }
--- request
GET /lua
--- response_body
2
3
5
7
--- no_error_log
[error]



=== TEST 6: coroutine.wrap(generate fib)
--- config
    location /lua {
        content_by_lua '
            function generatefib (n)
              return coroutine.wrap(function ()
                local a,b = 1, 1
                while a <= n do
                  coroutine.yield(a)
                  a, b = b, a+b
                end
              end)
            end

            -- In lua, because OP_TFORLOOP uses luaD_call to execute the iterator function,
            -- and luaD_call is a C function, so we can not yield in the iterator function.
            -- So the following case(using for loop) will be failed.
            -- Luajit is OK.
            if package.loaded["jit"] then
                for i in generatefib(1000) do ngx.say(i) end
            else
                local gen = generatefib(1000)
                while true do
                    local i = gen()
                    if not i then break end
                    ngx.say(i)
                end
            end
        ';
    }
--- request
GET /lua
--- response_body
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
--- no_error_log
[error]



=== TEST 7: coroutine wrap and cosocket
--- config
    resolver $TEST_NGINX_RESOLVER ipv6=off;
    location /lua {
        content_by_lua '
            function worker(url)
                local sock = ngx.socket.tcp()
                local ok, err = sock:connect(url, 80)
                coroutine.yield()
                if not ok then
                    ngx.say("failed to connect to: ", url, " error: ", err)
                    return
                end
                coroutine.yield()
                ngx.say("successfully connected to: ", url)
                sock:close()
            end

            local urls = {
                "agentzh.org",
                "iscribblet.org",
                "openresty.org"
            }

            local cfs = {}
            for i, url in ipairs(urls) do
                local cf = coroutine.wrap(function() worker(url) end)
                cfs[#cfs+1] = cf
            end

            for i=1,3 do cfs[i]() end
            for i=1,3 do cfs[i]() end
            for i=1,3 do cfs[i]() end

            ngx.say("*** All Done ***")
        ';
    }
--- request
GET /lua
--- response_body
successfully connected to: agentzh.org
successfully connected to: iscribblet.org
successfully connected to: openresty.org
*** All Done ***
--- no_error_log
[error]
--- timeout: 10



=== TEST 8: coroutine status, running
--- config
    location /lua {
        content_by_lua '
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield
            local st, rn = coroutine.status, coroutine.running

            function f(self)
                local cnt = 0
                if rn() ~= self then ngx.say("error"); return end
                ngx.say("running: ", st(self)) --running
                cy()
                local c = cc(function(father)
                    ngx.say("normal: ", st(father))
                end) -- normal
                cr(c, self)
            end

            local c = cc(f)
            ngx.say("suspended: ", st(c)) -- suspended
            cr(c, c)
            ngx.say("suspended: ", st(c)) -- suspended
            cr(c, c)
            ngx.say("dead: ", st(c)) -- dead
        ';
    }
--- request
GET /lua
--- response_body
suspended: suspended
running: running
suspended: suspended
normal: normal
dead: dead
--- no_error_log
[error]



=== TEST 9: entry coroutine yielded will be resumed immediately
--- config
    location /lua {
        content_by_lua '
            ngx.say("[", {coroutine.yield()}, "]")
            ngx.say("[", {coroutine.yield(1, "a")}, "]")
            ngx.say("done")
        ';
    }
--- request
GET /lua
--- response_body
[]
[]
done
--- no_error_log
[error]



=== TEST 10: thread traceback (multi-thread)
--- config
    location /lua {
        content_by_lua '
            local f = function(cr) coroutine.resume(cr) end
            -- emit a error
            local g = function() unknown.unknown = 1 end
            local l1 = coroutine.create(f)
            local l2 = coroutine.create(g)
            coroutine.resume(l1, l2)
            ngx.say("hello")
        ';
    }
--- request
GET /lua
--- response_body
hello
--- error_log eval
["stack traceback:", "coroutine 0:", "coroutine 1:", "coroutine 2:"]



=== TEST 11: thread traceback (only the entry thread)
--- config
    location /lua {
        content_by_lua '
            -- emit a error
            unknown.unknown = 1
            ngx.say("hello")
        ';
    }
--- request
GET /lua
--- error_code: 500
--- error_log eval
["stack traceback:", "coroutine 0:"]



=== TEST 12: bug: resume dead coroutine with args
--- config
    location /lua {
        content_by_lua '
            function print(...)
                local args = {...}
                local is_first = true
                for i,v in ipairs(args) do
                    if is_first then
                        is_first = false
                    else
                        ngx.print(" ")
                    end
                    ngx.print(v)
                end
                ngx.print("\\\n")
            end

            function foo (a)
                print("foo", a)
                return coroutine.yield(2*a)
            end

            co = coroutine.create(function (a,b)
                    print("co-body", a, b)
                    local r = foo(a+1)
                    print("co-body", r)
                    local r, s = coroutine.yield(a+b, a-b)
                    print("co-body", r, s)
                    return b, "end"
                end)

            print("main", coroutine.resume(co, 1, 10))
            print("main", coroutine.resume(co, "r"))
            print("main", coroutine.resume(co, "x", "y"))
            print("main", coroutine.resume(co, "x", "y"))
        ';
    }
--- request
GET /lua
--- response_body
co-body 1 10
foo 2
main true 4
co-body r
main true 11 -9
co-body x y
main true 10 end
main false cannot resume dead coroutine
--- no_error_log
[error]



=== TEST 13: deeply nested coroutines
--- config
    location /lua {
        content_by_lua '
            local create = coroutine.create
            local resume = coroutine.resume
            local yield = coroutine.yield
            function f()
                ngx.say("f begin")
                yield()
                local c2 = create(g)
                ngx.say("1: resuming c2")
                resume(c2)
                ngx.say("2: resuming c2")
                resume(c2)
                yield()
                ngx.say("3: resuming c2")
                resume(c2)
                ngx.say("f done")
            end

            function g()
                ngx.say("g begin")
                yield()
                ngx.say("g going")
                yield()
                ngx.say("g done")
            end

            local c1 = create(f)
            ngx.say("1: resuming c1")
            resume(c1)
            ngx.say("2: resuming c1")
            resume(c1)
            ngx.say("3: resuming c1")
            resume(c1)
            ngx.say("main done")
        ';
    }
--- request
GET /lua
--- response_body
1: resuming c1
f begin
2: resuming c1
1: resuming c2
g begin
2: resuming c2
g going
3: resuming c1
3: resuming c2
g done
f done
main done
--- no_error_log
[error]



=== TEST 14: using ngx.exit in user coroutines
--- config
    location /lua {
        content_by_lua '
            local create = coroutine.create
            local resume = coroutine.resume
            local yield = coroutine.yield

            local code = 400

            function f()
                local c2 = create(g)
                yield()
                code = code + 1
                resume(c2)
                yield()
                resume(c2)
            end

            function g()
                code = code + 1
                yield()
                code = code + 1
                ngx.exit(code)
            end

            local c1 = create(f)
            resume(c1)
            resume(c1)
            resume(c1)
            ngx.say("done")
        ';
    }
--- request
GET /lua
--- stap eval: $::StapScript
--- stap_out
create 2 in 1
resume 2 in 1
create 3 in 2
yield 2 in 1
resume 2 in 1
resume 3 in 2
yield 3 in 2
yield 2 in 1
resume 2 in 1
resume 3 in 2
exit

--- response_body_like: 403 Forbidden
--- error_code: 403
--- no_error_log
[error]



=== TEST 15: using ngx.exec in user coroutines
--- config
    location /lua {
        content_by_lua '
            local create = coroutine.create
            local resume = coroutine.resume
            local yield = coroutine.yield

            local code = 0

            function f()
                local c2 = create(g)
                yield()
                code = code + 1
                resume(c2)
                yield()
                resume(c2)
            end

            function g()
                code = code + 1
                yield()
                code = code + 1
                ngx.exec("/n/" .. code)
            end

            local c1 = create(f)
            resume(c1)
            resume(c1)
            resume(c1)
            ngx.say("done")
        ';
    }

    location ~ '^/n/(\d+)' {
        echo "num: $1";
    }

--- stap eval: $::StapScript
--- stap_out
create 2 in 1
resume 2 in 1
create 3 in 2
yield 2 in 1
resume 2 in 1
resume 3 in 2
yield 3 in 2
yield 2 in 1
resume 2 in 1
resume 3 in 2
exec

--- request
GET /lua
--- response_body
num: 3
--- no_error_log
[error]



=== TEST 16: coroutine.create in header_filter_by_lua
--- config
    location /lua {
        echo hello;
        header_filter_by_lua '
            function f()
                yield()
            end

            local c1 = coroutine.create(f)
            ngx.say("done")
        ';
    }
--- request
GET /lua
--- ignore_response
--- error_log
API disabled in the context of header_filter_by_lua*



=== TEST 17: resume coroutines from within another one that is not its parent
--- config
    location /t {
        content_by_lua '
            local print = ngx.say

            local c1, c2

            function f()
                print("f 1")
                print(coroutine.resume(c2))
                print("f 2")
            end

            function g()
                print("g 1")
                -- print(coroutine.resume(c1))
                print("g 2")
            end

            c1 = coroutine.create(f)
            c2 = coroutine.create(g)

            coroutine.resume(c1)
        ';
    }
--- request
GET /t
--- response_body
f 1
g 1
g 2
true
f 2
--- no_error_log
[error]



=== TEST 18: infinite recursive calls of coroutine.resume
--- config
    location /t {
        content_by_lua '
            local print = ngx.say

            local c1, c2

            function f()
                print("f 1")
                print(coroutine.resume(c2))
                print("f 2")
            end

            function g()
                print("g 1")
                print(coroutine.resume(c1))
                print("g 2")
            end

            c1 = coroutine.create(f)
            c2 = coroutine.create(g)

            coroutine.resume(c1)
        ';
    }
--- request
GET /t
--- stap2 eval: $::StapScript
--- response_body
f 1
g 1
falsecannot resume normal coroutine
g 2
true
f 2
--- no_error_log
[error]



=== TEST 19: resume running (entry) coroutines
--- config
    location /t {
        content_by_lua '
            ngx.say(coroutine.status(coroutine.running()))
            ngx.say(coroutine.resume(coroutine.running()))
        ';
    }
--- request
GET /t
--- response_body
running
falsecannot resume running coroutine
--- no_error_log
[error]



=== TEST 20: resume running (user) coroutines
--- config
    location /t {
        content_by_lua '
            local co
            function f()
                ngx.say("f: ", coroutine.status(co))
                ngx.say("f: ", coroutine.resume(co))
            end
            co = coroutine.create(f)
            ngx.say("chunk: ", coroutine.status(co))
            ngx.say("chunk: ", coroutine.resume(co))
        ';
    }
--- request
GET /t
--- response_body
chunk: suspended
f: running
f: falsecannot resume running coroutine
chunk: true
--- no_error_log
[error]



=== TEST 21: user coroutine end with errors, and the parent coroutine gets the right status
--- config
    location /t {
        content_by_lua '
            local co
            function f()
                error("bad")
            end
            co = coroutine.create(f)
            ngx.say("child: resume: ", coroutine.resume(co))
            ngx.say("child: status: ", coroutine.status(co))
            ngx.say("parent: status: ", coroutine.status(coroutine.running()))
        ';
    }
--- request
GET /t
--- response_body eval
qr/^child: resume: falsecontent_by_lua\(nginx\.conf:\d+\):4: bad
child: status: dead
parent: status: running
$/s
--- error_log eval
qr/lua coroutine: runtime error: content_by_lua\(nginx\.conf:\d+\):4: bad/



=== TEST 22: entry coroutine is yielded by hand and still gets the right status
--- config
    location /t {
        content_by_lua '
            local co = coroutine.running()
            ngx.say("status: ", coroutine.status(co))
            coroutine.yield(co)
            ngx.say("status: ", coroutine.status(co))
        ';
    }
--- request
GET /t
--- response_body
status: running
status: running
--- no_error_log
[error]



=== TEST 23: github issue #208: coroutine as iterator doesn't work
--- config
    location = /t {
        content_by_lua '
            local say = ngx.say
            local wrap, yield = coroutine.wrap, coroutine.yield

            local function it(it_state)
              for i = 1, it_state.i do
                yield(it_state.path, tostring(i))
              end
              return nil
            end

            local function it_factory(path)
              local it_state = { i = 10, path = path }
              return wrap(it), it_state
            end

            --[[
            for path, value in it_factory("test") do
              say(path, value)
            end
            ]]

            do
              local f, s, var = it_factory("test")
              while true do
                local path, value = f(s, var)
                var = path
                if var == nil then break end
                say(path, value)
              end
            end
        ';
    }
--- request
    GET /t
--- more_headers
Cookie: abc=32
--- stap2 eval: $::StapScript
--- response_body
test1
test2
test3
test4
test5
test6
test7
test8
test9
test10
--- no_error_log
[error]



=== TEST 24: init_by_lua + our own coroutines in content_by_lua
--- http_config
    init_by_lua 'return';
--- config
    resolver $TEST_NGINX_RESOLVER ipv6=off;
    location /lua {
        content_by_lua '
            function worker(url)
                local sock = ngx.socket.tcp()
                local ok, err = sock:connect(url, 80)
                coroutine.yield()
                if not ok then
                    ngx.say("failed to connect to: ", url, " error: ", err)
                    return
                end
                coroutine.yield()
                ngx.say("successfully connected to: ", url)
                sock:close()
            end

            local urls = {
                "agentzh.org",
            }

            local ccs = {}
            for i, url in ipairs(urls) do
                local cc = coroutine.create(function() worker(url) end)
                ccs[#ccs+1] = cc
            end

            while true do
                if #ccs == 0 then break end
                local cc = table.remove(ccs, 1)
                local ok = coroutine.resume(cc)
                if ok then
                    ccs[#ccs+1] = cc
                end
            end

            ngx.say("*** All Done ***")
        ';
    }
--- request
GET /lua
--- response_body
successfully connected to: agentzh.org
*** All Done ***
--- no_error_log
[error]
--- timeout: 10



=== TEST 25: init_by_lua_file + our own coroutines in content_by_lua
--- http_config
    init_by_lua_file html/init.lua;

--- config
    resolver $TEST_NGINX_RESOLVER ipv6=off;
    location /lua {
        content_by_lua '
            function worker(url)
                local sock = ngx.socket.tcp()
                local ok, err = sock:connect(url, 80)
                coroutine.yield()
                if not ok then
                    ngx.say("failed to connect to: ", url, " error: ", err)
                    return
                end
                coroutine.yield()
                ngx.say("successfully connected to: ", url)
                sock:close()
            end

            local urls = {
                "agentzh.org"
            }

            local ccs = {}
            for i, url in ipairs(urls) do
                local cc = coroutine.create(function() worker(url) end)
                ccs[#ccs+1] = cc
            end

            while true do
                if #ccs == 0 then break end
                local cc = table.remove(ccs, 1)
                local ok = coroutine.resume(cc)
                if ok then
                    ccs[#ccs+1] = cc
                end
            end

            ngx.say("*** All Done ***")
        ';
    }
--- user_files
>>> init.lua
return

--- request
GET /lua
--- response_body
successfully connected to: agentzh.org
*** All Done ***
--- no_error_log
[error]
--- timeout: 10



=== TEST 26: mixing coroutine.* API between init_by_lua and other contexts (github #304) - init_by_lua
--- http_config
    init_by_lua '
          co_wrap = coroutine.wrap
          co_yield = coroutine.yield
    ';

--- config
    location /cotest {
        content_by_lua '
            function generator()
                return co_wrap(function()
                    co_yield("data")
                end)
            end

            local co = generator()
            local data = co()
            ngx.say(data)
        ';
    }

--- request
GET /cotest
--- stap2 eval: $::StapScript
--- response_body
data
--- no_error_log
[error]



=== TEST 27: mixing coroutine.* API between init_by_lua and other contexts (github #304) - init_by_lua_file
--- http_config
    init_by_lua_file html/init.lua;

--- config
    location /cotest {
        content_by_lua '
            function generator()
                return co_wrap(function()
                    co_yield("data")
                end)
            end

            local co = generator()
            local data = co()
            ngx.say(data)
        ';
    }

--- user_files
>>> init.lua
co_wrap = coroutine.wrap
co_yield = coroutine.yield

--- request
GET /cotest
--- stap2 eval: $::StapScript
--- response_body
data
--- no_error_log
[error]



=== TEST 28: coroutine context collicisions
--- config
    location /lua {
        content_by_lua '
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield

            function f()
                return 3
            end

            for i = 1, 10 do
                collectgarbage()
                local c = cc(f)
                if coroutine.status(c) == "dead" then
                    ngx.say("found a dead coroutine")
                    return
                end
                cr(c)
            end
            ngx.say("ok")
        ';
    }
--- request
GET /lua
--- stap2 eval: $::StapScript
--- response_body
ok
--- no_error_log
[error]



=== TEST 29: require "coroutine"
--- config
    location /lua {
        content_by_lua '
            local coroutine = require "coroutine"
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield

            function f()
                local cnt = 0
                for i = 1, 20 do
                    ngx.say("Hello, ", cnt)
                    ngx.sleep(0.001)
                    cy()
                    cnt = cnt + 1
                end
            end

            local c = cc(f)
            for i=1,3 do
                cr(c)
                ngx.say("***")
            end
        ';
    }
--- request
GET /lua
--- stap2 eval: $::StapScript
--- response_body
Hello, 0
***
Hello, 1
***
Hello, 2
***
--- no_error_log
[error]



=== TEST 30: basic coroutine in header_filter_by_lua
--- config
    location = /t {
        echo ok;
        header_filter_by_lua '
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield

            function f()
                local cnt = 0
                for i = 1, 20 do
                    print("co yield: ", cnt)
                    cy()
                    cnt = cnt + 1
                end
            end

            local c = cc(f)
            for i = 1, 3 do
                print("co resume.")
                cr(c)
            end
        ';
    }
--- request
GET /t
--- response_body
ok
--- grep_error_log eval: qr/co (?:yield: \d+|resume\.)/
--- grep_error_log_out
co resume.
co yield: 0
co resume.
co yield: 1
co resume.
co yield: 2
--- no_error_log
[error]



=== TEST 31: basic coroutine in body_filter_by_lua
--- config
    location = /t {
        echo ok;
        body_filter_by_lua '
            local cc, cr, cy = coroutine.create, coroutine.resume, coroutine.yield

            function f()
                local cnt = 0
                for i = 1, 20 do
                    print("co yield: ", cnt)
                    cy()
                    cnt = cnt + 1
                end
            end

            local c = cc(f)
            for i = 1, 3 do
                print("co resume.")
                cr(c)
            end
        ';
    }
--- request
GET /t
--- response_body
ok
--- grep_error_log eval: qr/co (?:yield: \d+|resume\.)/
--- grep_error_log_out
co resume.
co yield: 0
co resume.
co yield: 1
co resume.
co yield: 2
co resume.
co yield: 0
co resume.
co yield: 1
co resume.
co yield: 2

--- no_error_log
[error]
