From da2598b5344a4dd4859e9f21efb1566d2043eb29 Mon Sep 17 00:00:00 2001 From: luaneko Date: Mon, 30 Jun 2025 22:01:00 +1000 Subject: [PATCH] Run all lua examples in docs as doctests --- crates/lb/src/net/mod.rs | 18 ++++++------ crates/lb/src/net/tcp.rs | 15 ++++++---- tests/main.lua | 61 ++++++++++++++++++++++++++++++---------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/crates/lb/src/net/mod.rs b/crates/lb/src/net/mod.rs index 1556892..27d5446 100644 --- a/crates/lb/src/net/mod.rs +++ b/crates/lb/src/net/mod.rs @@ -224,9 +224,9 @@ impl lb_netlib { /// /// ```lua /// local net = require("lb:net") - /// local socket = net.bind_tcp("127.0.0.1", 8080) + /// local socket = net.bind_tcp("127.0.0.1") /// - /// assert(socket:local_addr() == net.socketaddr("127.0.0.1:8080")) + /// assert(socket:local_addr():ip() == net.ipaddr("127.0.0.1")) /// socket:set_nodelay(true) /// ``` pub extern "Lua" fn bind_tcp( @@ -257,15 +257,16 @@ impl lb_netlib { /// /// # Example /// - /// ```lua + /// ```lua,no_run /// local net = require("lb:net") - /// local listener = net.listen_tcp("127.0.0.1", 1234) + /// local listener = net.listen_tcp("127.0.0.1") /// - /// assert(listener:local_addr() == net.socketaddr("127.0.0.1:1234")) + /// assert(listener:local_addr():ip() == net.ipaddr("127.0.0.1")) /// /// for stream in listener do - /// print("client connected: ", stream:remote_addr()) + /// print("client connected: ", stream:peer_addr()) /// end + /// ``` pub extern "Lua" fn listen_tcp( addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>, port: Option, @@ -287,9 +288,10 @@ impl lb_netlib { /// /// ```lua /// local net = require("lb:net") - /// local stream = net.connect_tcp("127.0.0.1", 1234) + /// local listener = net.listen_tcp("127.0.0.1") + /// local stream = net.connect_tcp("127.0.0.1", listener:local_addr():port()) /// - /// assert(stream:remote_addr() == net.socketaddr("127.0.0.1:1234")) + /// assert(stream:peer_addr():ip() == net.ipaddr("127.0.0.1")) /// stream:write("Hello, server!\n") /// ``` pub async extern "Lua" fn connect_tcp( diff --git a/crates/lb/src/net/tcp.rs b/crates/lb/src/net/tcp.rs index a573b4d..04161e9 100644 --- a/crates/lb/src/net/tcp.rs +++ b/crates/lb/src/net/tcp.rs @@ -183,7 +183,7 @@ impl lb_tcpsocket { /// /// This examples spawns a reader task and a writer task to operate on the stream concurrently. /// -/// ```lua +/// ```lua,no_run /// local task = require("lb:task") /// local net = require("lb:net") /// local socket = net.connect_tcp("127.0.0.1:1234") @@ -194,17 +194,17 @@ impl lb_tcpsocket { /// local reader = spawn(function() /// for chunk in socket, 1024 do /// print("received: ", chunk) -/// done +/// end /// /// print("done reading") /// end) /// /// local writer = spawn(function() /// for i = 1, 10 do -/// local msg = ("message %d\n"):format(i) +/// local msg = ("message %d"):format(i) /// socket:write(msg) /// print("sent: ", msg) -/// done +/// end /// /// print("done writing") /// end) @@ -215,7 +215,10 @@ impl lb_tcpsocket { /// The above example uses the socket as an iterator in a generic `for` loop to read data in chunks /// of up to 1024 bytes. It is equivalent to the following: /// -/// ```lua +/// ```lua,no_run +/// local net = require("lb:net") +/// local socket = net.connect_tcp("127.0.0.1:1234") +/// /// while true do /// local chunk = socket:read_partial(1024) /// if chunk == nil then break end @@ -430,7 +433,7 @@ impl lb_tcpstream { /// /// The listener can be used as an iterator in a generic `for` loop to accept incoming connections: /// -/// ```lua +/// ```lua,no_run /// local net = require("lb:net") /// local listener = net.listen_tcp("127.0.0.1") /// diff --git a/tests/main.lua b/tests/main.lua index 82ae3cd..27901de 100644 --- a/tests/main.lua +++ b/tests/main.lua @@ -12,6 +12,7 @@ local color = { reset = "\x1b[0m", pass = "\x1b[32;1m", -- green fail = "\x1b[31;1m", -- red + faint = "\x1b[2;39;49m", -- faint } local icon = { @@ -24,6 +25,12 @@ local function style(name, s) return ("%s%s%s"):format(color[name], s, color.reset) end +local function rjust(s, w) + if w == nil then w = 12 end + if #s >= w then return s end + return (" "):rep(w - #s) .. s +end + local function create_test(name, f, group) local test = { type = "test", name = name or "", group = group, state = "pending", f = f } local fenv = setmetatable({}, { __index = global, __newindex = global }) @@ -67,13 +74,13 @@ local function trace(msg) end local function run_test(test) - local ok, res = xpcall(test.f, trace, test) + local ok, trace = xpcall(test.f, trace, test) if ok then test.state = "pass" - print("", ("%s %s"):format(style("pass", "PASS"), name_test(test))) + print(("%s %s"):format(style("pass", rjust("PASS")), name_test(test))) else test.state = "fail" - print("", ("%s %s\n\n%s\n"):format(style("fail", "FAIL"), name_test(test), res)) + print(("%s %s\n\n%s\n"):format(style("fail", rjust("!!! FAIL")), name_test(test), trace)) end collectgarbage() -- gc after each test to test destructors return test @@ -118,28 +125,24 @@ local function main(item) end end local elapsed = time:elapsed_secs() - local code = 1 + local retcode if fail == 0 then - print("", style("pass", ("%s %d tests passed"):format(icon.check, pass))) - code = 0 + print(style("pass", ("\t%s %d tests passed"):format(icon.check, pass))) + retcode = 0 else print( - "", - ("%s, %s"):format( + ("\t%s, %s"):format( style("pass", ("%s %d tests passed"):format(icon.check, pass)), style("fail", ("%s %d tests failed"):format(icon.cross, fail)) ) ) + retcode = 1 end - if elapsed < 1000 then - print("", ("%s completed in %.2f ms"):format(icon.chevron, elapsed * 1000)) - else - print("", ("%s completed in %.2f s"):format(icon.chevron, elapsed)) - end + print(style("faint", ("\t%s completed in %.2fs"):format(icon.chevron, elapsed))) cx = nil collectgarbage() - check_refs() - return code -- report error to cargo + check_refs() -- check that all refs were properly unref'ed in destructors + return retcode -- report error to cargo end return main(create_group("", function() @@ -152,6 +155,34 @@ return main(create_group("", function() end end + local function include_doctest(path, pat) + for entry in fs.glob_dir(path, pat) do + local line, doctest = 0, nil + for s in fs.read(entry:path()):gmatch("([^\n]*)\n?") do + line = line + 1 + local prefix = s:match("^%s*///") + s = prefix and s:sub(#prefix + 1) + if s and not s:match("^%s*```%s*$") then + if s:match("^%s*```lua$") then + doctest = { line = line, col = #prefix + 2 } + elseif doctest then + table.insert(doctest, (" "):rep(#prefix) .. s) + end + else + if doctest then + local name = ("%s:%d:%d"):format(entry:path(), doctest.line, doctest.col) + local f, err = loadstring(table.concat(doctest, "\n"), "@" .. name) + if not f then error(err) end + test(("%s %s"):format(name, style("faint", "(doctest)")), f) + end + doctest = nil + end + end + end + end + include("tests", "**/*.lua") include("crates", "*/tests/**/*.lua") + include_doctest("src", "**/*.rs") + include_doctest("crates", "*/src/**/*.rs") end))