☰
libwebsockets 持续集成与 CTest 测试体系实战指南:从 Sai 调度到新测试接入
2026/10/13 12:05:07 网站建设 项目流程
  • 后端
  • 网络
  • 通信

【免费下载链接】libwebsockets

canonical libwebsockets.org networking library

项目地址:https://gitcode.com/gh_mirrors/li/libwebsockets
点击查看免费下载

导读

libwebsockets(lws)作为覆盖 WebSocket、HTTP/1.1/2/3、MQTT、Secure Streams 等众多协议栈的网络库,其功能正确性长期依赖一套可持续演进的自动化测试体系。本文以仓库官方文档 READMEs/README.ci.md 为主线,系统讲解 lws 的 CI 组织方式:Sai 如何调度各平台构建与测试、CTest 如何注册并执行 api-tests 与 minimal examples 自测、如何在无 root 权限的环境下手动复现整套测试,以及为新增功能编写测试时在超时、工作目录、后台伴侣进程(buddy/fixture)与并发端口分配上的工程规范。读完本文,你将掌握在 lws 中新增一个可被 CI 持续守护的测试的完整方法。

为什么 lws 坚持把新功能写进 CI

文档 READMEs/README.ci.md 开门见山给出了一个朴素而重要的理由:凡是期望长期持续工作的东西,都应该在 CI 中被反复检验。项目维护者的经验是,某个功能如果使用者很少,那么一次重构或周边改动就可能把它破坏到不可用状态,而且往往在很久之后才被人察觉——因为没人跑它。

因此,lws 的工程约定是:新增任何有一定复杂度(nontrivial)的代码时,都尽量附带对应的 CI 测试。这被作者自嘲为“非生产性的苦差事(PITA)”,但从仓库中数百个以st_/ki_/api-test-前缀注册的 ctest 用例(见下节)可以看出,这一约定已被严格执行:每一次协议栈改动,都有对应测试在同一批构建机群上反复回归。

lws CI 的三大集成点

lws 的 CI 由三个层次构成,分别负责“调度”“执行”与“专项补测”。

Sai:跨平台构建与测试的调度器

CI 由 Sai 驱动运行,调度配置存放在仓库根目录的 .sai.json 中。该文件把每一个“平台/配置”组合映射为三件事:

  1. 该平台/配置需要的 CMake 选项(cmake字段);
  2. 它要执行的构建与测试步骤(build字段);
  3. 驱动 api-tests 与 minimal examples 的ctest调用。

从实际文件看,platforms覆盖了 rockylinux(gcc/clang)、Fedora、Ubuntu(含 riscv64、树莓派 aarch64)、NetBSD/OSX(Intel 与 Apple Silicon)、FreeBSD、Windows(msvc、msvc-schannel、msvc-mbedtls)以及 ESP32 交叉构建与 Coverity 静态扫描等十余类构建机。每个平台的build数组典型地包含三条步骤:

"mkdir -p build destdir; cd build; CCACHE_DISABLE=1 LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib cmake .. ${cmake}", "cd build && make -j$SAI_PARALLEL && rm -rf ../destdir && make -j$SAI_PARALLEL DESTDIR=../destdir install", "cd build && LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib ctest -j$SAI_PARALLEL --output-on-failure --repeat until-pass:3"

注意第三条命令的三个细节:

  • -j$SAI_PARALLEL:Sai 传入的并行度,ctest 本身会被并发执行;
  • --output-on-failure:只有失败时才倾泻测试日志,保持正常输出可读;
  • --repeat until-pass:3:对偶发失败(如网络抖动)做最多 3 次重试,避免把瞬时问题误判为回归。

configurations一节则罗列了数十种编译维度组合,例如mbedtls、gnutls、bearssl、asan(ASan+LSan+UBSan)、sansio(sans-IO 拆分链接检查)、coverage(gcov 覆盖率统计)、fuzz(libFuzzer 持续模糊测试)、oauth(完整登录链路端到端)等,每个配置可再绑定到特定的平台子集。

ctest:测试用例的注册与执行枢纽

minimal-examples-lowlevel/api-tests下的 api-tests 与 minimal examples 的自测用例,都是通过各自的 CMakeLists.txt 注册进ctest的。因此新增一个测试,只需要把它接入该目录下的 CMake 注册逻辑,就能在每一个启用了对应功能的 Sai 构建机上自动执行。

机制上,minimal-examples-lowlevel/CMakeLists.txt 会用 GLOB 遍历minimal-examples-lowlevel下每个二级子目录,凡是存在CMakeLists.txt的目录都通过add_subdirectory()纳入构建(还支持用LWS_WITH_MINIMAL_FILTER_YES/LWS_WITH_MINIMAL_FILTER_NO过滤)。而每个示例自己的 CMakeLists 里,像 api-test-lejp/CMakeLists.txt 这样简单的用例,注册只需一行:

add_executable(${PROJECT_NAME} main.c) add_test(NAME api-test-lejp COMMAND lws-api-test-lejp)

也就是说,一个测试的本质是:运行某个可执行文件,进程以退出码 0 结束即视为成功,任何非 0 退出码都视为失败。

附加测试脚本:默认 ctest 之外的专项补测

scripts/h2spec.sh 及其同类脚本(scripts/autobahn-test-server.sh、scripts/autobahn-test-client.sh、scripts/h2load.sh 等)是独立脚本:它们从构建目录运行,针对已安装(installed)的构建产物执行,但不属于默认 ctest 流程。

以 h2spec.sh 为例,它从 GitHub 下载 h2spec 工具,在后台拉起lws-minimal-http-server-tls,对127.0.0.1:7681跑 HTTP/2 规范测试,再检查输出中是否出现Failures:来决定成败;autobahn 脚本则用wstest对本地 echo 服务器执行完整的 RFC6455 WebSocket 一致性测试(排除 2.10/2.11 两个 RFC 未作要求的用例),并分别统计“lws 作为 server”与“lws 作为 client”两个方向的通过情况。这类脚本适合做规范级大范围扫描,但不适合放进每次都跑的默认回归里。

在本地手动跑一遍完整 ctest

先决条件:更新老旧的 cmake

lws 的 ctest 用例依赖较新的 CMake 特性(fixture、$<TARGET_FILE>生成器表达式、文件锁等)。若发行版自带 cmake 过老,需要升级。文档给出的 Kitware 源方案适用于 Ubuntu bionic/xenial:

# apt purge --auto-remove cmake # apt install gnupg wget apt-transport-https ca-certificates # wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add - # apt edit-sources

在源列表末尾加入deb https://apt.kitware.com/ubuntu/ bionic main(按需把bionic换成xenial)保存退出,然后:

# apt update # apt install cmake

测试规则写在 CMakeLists.txt 里

测试的规则以 ctest/cmake 语法写在各 minimal examples 与 api tests 的 CMakeLists 中,且这些示例只在对应构建选项开启时才会被启用。因此构建时必须加上:

cmake .. -DLWS_WITH_MINIMAL_EXAMPLES=1

这一点与主 CMakeLists.txt 中LWS_WITH_MINIMAL_EXAMPLES选项的定义一致(见该文件第 240 行附近的 CTest 选项段)。

生成与准备:无特权侧装到 DESTDIR

设计约束是:测试必须在无 root、且不干扰构建机上任何其他 lws 安装的前提下运行。为此官方流程要求先做一次无特权“侧装”(side-install):

build $ make -j12 && \ rm -rf ../destdir && \ make -j12 DESTDIR=../destdir install && \ LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins ctest -j2 --output-on-failure

各步骤的含义:

  • rm -rf ../destdir:清掉上次的侧装残留,确保测试对象是本次刚构建的产物;
  • make DESTDIR=../destdir install:把库、可执行文件与插件装进构建目录旁的destdir,不触碰系统目录;
  • 设置LD_LIBRARY_PATH:确保运行的是我们刚构建的.so与插件,而不是机器上可能存在的其他 lws 版本的同名库。文档特别点名了插件:新构建的插件必须通过搜索路径优先被加载,这里把libwebsockets-test-server/plugins目录排在最前。

Windows 上的等价命令是ctest . -C DEBUG(若构建类型是 Release 则用-C RELEASE)。

一份正常的运行结果长什么样

文档给出了 73 个测试全绿的完整输出,这里截取关键片段说明其形态(哪些测试能跑,取决于你的构建选项):

Test project /projects/libwebsockets/build Start 71: st_wcs_srv Start 43: st_hcp_srv 1/73 Test #71: st_wcs_srv .................................. Passed 5.01 sec 2/73 Test #43: st_hcp_srv .................................. Passed 5.01 sec ... 72/73 Test #66: ss-tf ....................................... Passed 55.51 sec Start 5: api-test-lws_smd 73/73 Test #5: api-test-lws_smd ............................ Passed 4.22 sec 100% tests passed, 0 tests failed out of 73 Total Test time (real) = 137.76 sec

可以看到测试分成几大类:

  • api-test-*:纯函数/解析器类自测,多为毫秒级(如api-test-lwsac0.00s、api-test-lejp0.00s);
  • st_*/ki_*:后台“启动(setup)”与“清理(cleanup)”伴侣进程的 fixture 用例;
  • http-client-multi*、sspc-*、ss-*等:网络端到端用例,耗时从零点几秒到数十秒不等;
  • 总耗时约 2 分 18 秒,全部通过。

一个关键环境变量:测试用本地服务器地址

由于测试只对本地对端运行,客户端用例需要知道往哪个地址连。主 CMakeLists.txt 第 257~266 行按 IPv4/IPv6 开关推导出LWS_CTEST_SERVER_RESOLVE(::1/127.0.0.1/localhost),并注入各示例的 CMakeLists 与 policy 模板(如 minimal-examples/client/ws-echo/policy-local.json.in 中的"endpoint": "@LWS_CTEST_SERVER_RESOLVE@"),保证测试始终打向本机回环地址,不需要任何公网连接。

创建新测试的四大工程规范

文档为“如何给新功能写测试”给出了四条必须遵守的规范,下面逐一结合仓库源码展开。

1. 显式设置 TIMEOUT

ctest 的默认超时是1500 秒,这对大多数用例都过松——一旦用例死锁,失败要等 25 分钟才暴露。因此规范要求:每个测试都要设置合适的TIMEOUT属性。

仓库实践是分层的:前台业务测试通常给 30~50 秒(如 minimal-http-client-multi/CMakeLists.txt 中TIMEOUT 50、POST 系列TIMEOUT 30);后台 fixture 启动因为要容忍慢速设备(如树莓派上 SD 卡 IO 抖动),给到TIMEOUT 800。

2. 设置 WORKING_DIRECTORY

  • 服务端测试程序通常需要从它自己的minimal-examples/...目录运行,才能访问index.html等资源文件;
  • 客户端程序在-DLWS_WITH_MBEDTLS=1构建下也必须从自身目录运行,因为 mbedTLS 构建需要额外读取 warmcat.com / libwebsockets.org 的受信 CA。

因此规范是:任何情况下都把WORKING_DIRECTORY设为示例应用所在的目录。例如 minimal-http-client-multi/CMakeLists.txt 第 192 行把工作目录指向minimal-examples-lowlevel/http-client/minimal-http-client-multi。

3. 用 fixture 机制“孵化”后台伴侣进程

大量网络测试需要一对“搭档”:要么先起一个服务器让客户端来打,要么先起一个客户端去连被测服务器。cmake 原生不支持把守护进程声明为测试依赖,lws 因此提供了两个辅助脚本:

  • scripts/ctest-background.sh:以后台方式拉起被测程序,把 pid 写入装饰过的 /tmp 文件(实际上按构建树定位,见下文),并负责在进程提前退出时倾泻其日志以便排障;
  • scripts/ctest-background-kill.sh:负责事后清理,先 SIGTERM、限时等不到退出再 SIGKILL,并同样会打印后台进程日志与异常退出的现场信息。

这两个脚本共同 source scripts/ctest-fixture-key.sh,后者解决了两个关键问题:

  1. pid/日志文件的定位:同一台机器上可能并行跑多个构建树(Sai 允许一个 builder 上多构建并发),所以 pid 与日志文件按“构建树”而不是按 fixture 名区分,存放在<build>/Testing/fixtures/下,命名形如$FIXTURE_EXE.$FIXTURE_NAME.$SAI_INSTANCE_IDX.pid/.log;构建树路径通过从可执行文件路径或工作目录向上查找CMakeCache.txt得到;
  2. 启动时的就绪确认:ctest-background.sh 支持SAI_LIST_PORT环境变量——当设置时,脚本会循环检查 TCP/UDP 端口是否开始监听(Linux 上用netstat或ss,macOS 上用lsof),最多等 30 秒,并且始终校验后台子进程仍然存活,防止“端口被陌生进程占着、自家子进程已死于 EADDRINUSE”的假启动;未设置该变量时则退化为 sleep 1 秒(valgrind 场景 sleep 15 秒)后做存活检查。脚本默认给后台程序追加-d1039日志级别,这同时也是让 lwsws 安装崩溃处理器的级别,使得 fixture 里的 SIGSEGV 会带着 backtrace 留在日志中而不是无声消失(可用LWS_CTEST_BG_DEBUG覆盖)。

有了脚本之后,要用 cmake 的fixture概念把“起”和“杀”绑定到被测用例上。文档以minimal-http-client-multi为例——它需要minimal-http-server-tls作为对端。第一步,注册两个“伪测试”分别负责启动与停止,fixture 名统一叫hcm_srv:

add_test(NAME st_hcm_srv COMMAND ${CMAKE_SOURCE_DIR}/scripts/ctest-background.sh hcm_srv $<TARGET_FILE:lws-minimal-http-server-tls> --port ${PORT_HCM_SRV} ) add_test(NAME ki_hcm_srv COMMAND ${CMAKE_SOURCE_DIR}/scripts/ctest-background-kill.sh hcm_srv $<TARGET_FILE_NAME:lws-minimal-http-server-tls> --port ${PORT_HCM_SRV})

第二步,用set_tests_properties把这两个伪测试标记为 fixturehcm_srv的 setup/cleanup,并给 setup 一个 800 秒超时、指定服务端的工作目录(TLS 服务器需要从自己的源码目录读取证书):

set_tests_properties(st_hcm_srv PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/http-server/minimal-http-server-tls FIXTURES_SETUP hcm_srv TIMEOUT 800) set_tests_properties(ki_hcm_srv PROPERTIES FIXTURES_CLEANUP hcm_srv)

第三步,让真正被测的用例声明依赖该 fixture:

set_tests_properties(http-client-multi PROPERTIES FIXTURES_REQUIRED "hcm_srv" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/http-client/minimal-http-client-multi TIMEOUT 50)

完成后,ctest 会自动在http-client-multi运行前启动hcm_srv、运行后将其杀掉,无需手工干预。仓库实际文件还做了更细的工程化:通过set_property(TEST st_hcm_srv PROPERTY ENVIRONMENT SAI_LIST_PORT=${PORT_HCM_SRV})把端口传给后台脚本做“端口就绪等待”,并用WILL_FAIL TRUE标注那些预期以非零退出码结束的负向用例(如单连接限制且不启用流水线时必然失败的http-client-multi-restrict-nopipe-fail)。

4. 伴侣套接字与测试并发:端口不能拍脑袋

用 TCP 套接字在同一个 VM 或 systemd-nspawn 网络上下文里跑本地伴侣测试时,不能直接用一个众所周知的端口(比如 7681)。原因有二:

  • ctest 本身通常并发执行(-j);
  • Sai 往往在同一台机器上并发构建多个实例(可能多达几十个),也就是同一个 VM 里可能同时跑着多个不同的 ctest。

不同测试可以各自约定端口区间,来解决“一个 ctest 内并发跑不同测试”的问题。而针对“多个 ctest 同时跑”的情况,环境变量$ENV{SAI_INSTANCE_IDX}(一个像 0、1、33 这样的序号)被用来进一步错开端口。文档给出的模式是:

# # instantiate the server per sai builder instance, they are running in the same # machine context in parallel so they can tread on each other otherwise # set(PORT_HCM_SRV "7670") if ("$ENV{SAI_INSTANCE_IDX}") math(EXPR PORT_HCM_SRV "7671 + $ENV{SAI_INSTANCE_IDX}") endif()

不跑 Sai 时,SAI_INSTANCE_IDX未设置,端口就用默认值——这正好对应“手动跑测试”的场景。

不过仓库演进后,这套手工偏移已被更自动化的机制取代:cmake 函数lws_get_free_port()(定义在 cmake/LwsCheckRequirements.cmake 第 140 行起)用基于文件锁的计数器(/tmp/lws_port_counter_$USER或 Windows 下%TEMP%对应文件)在 15000~30000 区间内顺序分配互不重复的端口;lws_get_free_ports(VAR_NAME COUNT)则一次性预留一段连续端口,供单个测试进程同时监听多个端口(如多阶段测试)时避免被并行兄弟用例“打洞”。minimal-http-client-multi/CMakeLists.txt 第 42~43 行就用了lws_get_free_port(PORT_HCM_SRV)与lws_get_free_port(PORT_HCMP_SRV)。

文档给这条规范下的最终建议非常务实:最好的做法是拷贝一个现成的简单用例(如 minimal-http-client 的 CMakeLists.txt),把名字和端口改成唯一的——因为这套 fixture + 端口 + 并发协调的组合足够复杂,从零手写容易出错。

把新测试接进 Sai 的完整路径小结

综合以上内容,一个新增功能要进入 lws 的 CI 守护,完整路径是:

  1. 在minimal-examples-lowlevel/api-tests/<new-test>/下写好测试程序main.c与CMakeLists.txt,用require_lws_config()声明它依赖的 lws 编译选项(见 cmake/LwsCheckRequirements.cmake),只有依赖满足时才add_executable+add_test注册;
  2. 若需要本地对端,按上述 fixture 三件套(setup 伪测试、cleanup 伪测试、FIXTURES_REQUIRED)接上ctest-background.sh/ctest-background-kill.sh,端口交给lws_get_free_port();
  3. 为每个用例设置合理的TIMEOUT与WORKING_DIRECTORY;
  4. 本地用-DLWS_WITH_MINIMAL_EXAMPLES=1构建后,按“无特权 DESTDIR 侧装 + LD_LIBRARY_PATH 指向前置路径 + ctest --output-on-failure”的流程验证通过;
  5. 推送后,Sai 会根据 .sai.json 在所有启用了相关功能的平台/配置组合上自动构建并执行该测试,--repeat until-pass:3兜底偶发失败。

遵循这套规范,你的新代码就会和仓库里上百个既有用例一样,在每次重构后被横跨 x86_64、aarch64、riscv64、Windows、macOS、ESP32 乃至 FreeBSD 的构建机群自动验证,从而兑现文档开头的承诺:让“各个功能都还能用”这句话持续为真。

  • 后端
  • 网络
  • 通信

【免费下载链接】libwebsockets

canonical libwebsockets.org networking library

项目地址:https://gitcode.com/gh_mirrors/li/libwebsockets
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询