-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local testimony = require("testimony") local lev = require("lev") local testify = testimony.new("== lev resource tracking ==") -- ========================================================================= -- own() -- ========================================================================= testify:that("own: returns the resource (chaining)", function() lev.run(function() local resource = { closed = false, close = function(self) self.closed = true end, } local ret = lev.own(resource) testimony.assert_equal(resource, ret) end) end) testify:that("own: auto-closes on normal exit", function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.run(function() local task = lev.spawn(function() lev.own(resource) return "done" end) lev.await(task) end) testimony.assert_true(resource.closed, "resource should be closed after task completes") end) testify:that("own: auto-closes on coroutine error", function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.run(function() local task = lev.spawn(function() lev.own(resource) error("boom") end) local ok, err = lev.await(task) testimony.assert_nil(ok) testimony.assert_match("boom", err) end) testimony.assert_true(resource.closed, "resource should be closed after task error") end) -- ========================================================================= -- defer() -- ========================================================================= testify:that("defer: runs on normal exit", function() local ran = false lev.run(function() local task = lev.spawn(function() lev.defer(function() ran = true end) return "ok" end) lev.await(task) end) testimony.assert_true(ran, "defer should have run on normal exit") end) testify:that("defer: runs on coroutine error", function() local ran = false lev.run(function() local task = lev.spawn(function() lev.defer(function() ran = true end) error("fail") end) lev.await(task) end) testimony.assert_true(ran, "defer should have run on error") end) -- ========================================================================= -- LIFO ordering -- ========================================================================= testify:that("LIFO: cleanups run in reverse registration order", function() local order = {} lev.run(function() local task = lev.spawn(function() lev.defer(function() order[#order + 1] = 1 end) lev.defer(function() order[#order + 1] = 2 end) lev.defer(function() order[#order + 1] = 3 end) end) lev.await(task) end) testimony.assert_equal(3, #order) testimony.assert_equal(3, order[1]) testimony.assert_equal(2, order[2]) testimony.assert_equal(1, order[3]) end) -- ========================================================================= -- disown() -- ========================================================================= testify:that("disown: prevents auto-cleanup", function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.run(function() local task = lev.spawn(function() lev.own(resource) lev.disown(resource) end) lev.await(task) end) testimony.assert_false(resource.closed, "disowned resource should NOT be closed") end) testify:that("disown: returns the resource", function() lev.run(function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.own(resource) local ret = lev.disown(resource) testimony.assert_equal(resource, ret) end) end) -- ========================================================================= -- Error isolation -- ========================================================================= testify:that("cleanup error isolation: one error doesn't block others", function() local first_ran = false local third_ran = false lev.run(function() local task = lev.spawn(function() lev.defer(function() first_ran = true end) lev.defer(function() error("cleanup boom") end) lev.defer(function() third_ran = true end) end) lev.await(task) end) -- LIFO: third runs first, then the error, then first testimony.assert_true(third_ran, "cleanup after error should still run") testimony.assert_true(first_ran, "cleanup before error should still run") end) -- ========================================================================= -- Double close safety -- ========================================================================= testify:that("double close safety: own + explicit close", function() local close_count = 0 local resource = { closed = false, close = function(self) if self.closed then return end self.closed = true close_count = close_count + 1 end, } lev.run(function() local task = lev.spawn(function() lev.own(resource) resource:close() -- explicit close -- auto-cleanup will also call close, but it's idempotent end) lev.await(task) end) testimony.assert_true(resource.closed, "resource should be closed") testimony.assert_equal(1, close_count, "close should only take effect once") end) -- ========================================================================= -- TCP socket cleanup on error -- ========================================================================= testify:that("tcp: socket cleaned up on task error", function() local sock_fd lev.run(function() local task = lev.spawn(function() local sock = lev.own(lev.tcp()) sock_fd = sock:fd() testimony.assert_true(sock_fd >= 0, "expected valid fd") error("handler crash") end) local ok, err = lev.await(task) testimony.assert_nil(ok) testimony.assert_match("handler crash", err) end) -- fd was closed by auto-cleanup; we can't directly verify the fd is -- closed from Lua, but we verify the cleanup ran without error testimony.assert_true(sock_fd >= 0, "fd was captured before error") end) -- ========================================================================= -- Detached task cleanup -- ========================================================================= testify:that("detached task cleanup: success path", function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.run(function() lev.spawn(function() lev.own(resource) lev.sleep(0.01) end, { detached = true }) lev.sleep(0.1) end) testimony.assert_true(resource.closed, "detached task should clean up on success") end) testify:that("detached task cleanup: error path", function() local resource = { closed = false, close = function(self) self.closed = true end, } lev.run(function() lev.spawn(function() lev.own(resource) lev.sleep(0.01) error("detached crash") end, { detached = true }) lev.sleep(0.1) end) testimony.assert_true(resource.closed, "detached task should clean up on error") end) -- ========================================================================= -- No-op outside lev.run() -- ========================================================================= testify:that("own/defer/disown: no-op outside lev.run()", function() local resource = { closed = false, close = function(self) self.closed = true end, } -- These should not error local ret = lev.own(resource) testimony.assert_equal(resource, ret) lev.defer(function() end) local ret2 = lev.disown(resource) testimony.assert_equal(resource, ret2) testimony.assert_false(resource.closed, "resource should not be touched outside lev.run()") end) -- ========================================================================= -- Mixed own + defer LIFO order -- ========================================================================= testify:that("mixed own + defer: LIFO order preserved", function() local order = {} local r1 = { close = function(_) order[#order + 1] = "r1" end, } local r2 = { close = function(_) order[#order + 1] = "r2" end, } lev.run(function() local task = lev.spawn(function() lev.own(r1) lev.defer(function() order[#order + 1] = "d1" end) lev.own(r2) lev.defer(function() order[#order + 1] = "d2" end) end) lev.await(task) end) -- Registration order: r1, d1, r2, d2 -- LIFO: d2, r2, d1, r1 testimony.assert_equal(4, #order) testimony.assert_equal("d2", order[1]) testimony.assert_equal("r2", order[2]) testimony.assert_equal("d1", order[3]) testimony.assert_equal("r1", order[4]) end) -- ========================================================================= -- Conclude -- ========================================================================= testify:conclude()