NocoDB 自动化安装向导的测试体系:noco.sh 的 bats 黄金文件测试与离线验证设计
本文围绕 NocoDB 仓库中 docker-compose/1_Auto_Upstall/tests/ 下的测试文档展开,讲清 noco.sh(NocoDB auto-upstall 安装向导)的自动化测试是如何在无 Docker、无网络、任意操作系统上运行的:基于 NOCO_SKIP_PREFLIGHT 环境变量将"文件生成"与"系统预检"解耦,再用 bats 黄金文件(golden-file)快照、标志校验、文档一致性比对和 expect 驱动的交互向导测试四套机制覆盖安装器的全部关键行为。读完本文,你将掌握该测试套件的运行方式、依赖要求、黄金文件再生流程,以及从源码印证的关键设计决策。
一、测试对象与总体设计
被测对象是 noco.sh——NocoDB 的单机安装向导。它的职责是:交互式(或全参数化非交互)地收集域名、Postgres、Redis 与 SSL 配置,然后在 ./nocodb/ 目录生成一套可直接 docker compose up -d 的部署文件(docker-compose.yml、docker.env、nocodb/db.json、update.sh、.gitignore),并顺带完成预检(Docker 是否存在、80/443 端口是否被占用、SELinux 检测)和拉镜像起栈。
由于安装器同时包含"系统副作用"(检查 Docker、占用端口、docker compose up -d)和"纯文件生成"两类逻辑,测试无法在 CI 里完整执行前者。为此,安装器在源码里内置了一个测试开关:
# noco.sh(第 44 行附近)
NOCO_SKIP_PREFLIGHT="${NOCO_SKIP_PREFLIGHT:-}" # when set, skip OS/Docker/port checks (testing & re-runs)
这个变量在三个预检/启动入口被检查,设置后即直接返回:
check_prereqs()(Docker / Compose / curl 存在性检查)开头:[ -n "$NOCO_SKIP_PREFLIGHT" ] && return 0;check_ports()(production 模式下 80/443 端口监听检查)开头同样短路返回;start_stack()(执行docker compose up -d的步骤)开头同样短路返回。
这意味着测试只需要 NOCO_SKIP_PREFLIGHT=1 bash noco.sh <flags> 就能在任何 OS 上拿到完整的生成产物,而不需要 Docker、不需要网络——这正是测试文档中所述的核心前提:
安装器的文件生成通过内部
NOCO_SKIP_PREFLIGHT环境变量与其 OS/Docker/端口预检解耦,因此整套快速测试可在任意操作系统上运行,无需 Docker、无需网络。
二、测试套件覆盖矩阵
测试目录按职责拆分为四个 bats 文件,文档中的覆盖矩阵如下(完整继承自 tests/README.md):
| 文件 | 检查内容 | 需要 Docker |
|---|---|---|
install/generate.bats |
按场景对生成的 docker-compose.yml 做黄金文件快照(local、production-ip、production-ssl、external-redis、external-pg ×3、fully-external),外加 db.json / docker.env / 文件权限 / 辅助文件检查 |
否 |
install/validation.bats |
非交互标志校验——缺失/不兼容的标志必须快速失败并给出清晰报错;--help 必须成功 |
否 |
install/examples.bats |
安装器生成的外部 Postgres db.json 必须与文档示例 docker-compose/examples/ 保持结构同步(jq 归一化后比较) |
否(需要 jq) |
install/interactive.bats |
交互向导的 UX 守卫测试,由 expect 驱动。受门控(gated) |
否 |
三、generate.bats:黄金文件快照测试
generate.bats 是套件的主力。它通过共享辅助函数 lib/helpers.bash 中的 generate <flags...> 把安装器跑进一个隔离的 mktemp -d 临时目录,然后逐项断言产物。
3.1 八个快照场景
每个场景对应一个提交在仓库中的期望输出 golden/<scenario>/docker-compose.yml,测试用 assert_golden <scenario> 做差异比对。场景与命令行参数的对应关系(与 regen-golden.sh 中的场景清单保持一致):
# 场景 1:本地模式(内置 PG + 内置 Redis,端口 8080)
generate --domain=localhost --pg=bundled --redis=bundled
# 场景 2:IP 直连的 production-ip(80 端口,无 traefik)
generate --domain=1.2.3.4 --pg=bundled --redis=bundled
# 场景 3:真实域名 production-ssl(traefik + Let's Encrypt)
generate --domain=demo.example.com --acme-email=ssl@nocodb.com --pg=bundled --redis=bundled
# 场景 4:外部 Redis
generate --domain=localhost --pg=bundled --redis=external --redis-url=redis://localhost:6379
# 场景 5/6/7:外部 PG 的三种 SSL 形态
generate --domain=localhost --pg=external --pg-host=db.example.com --pg-user=nocodb \
--pg-password=secretpass --pg-ssl=managed --redis=bundled # 托管 CA(RDS/云 SQL)
generate ... --pg-ssl=none ... # 无 SSL
generate ... --pg-ssl="$(fake_ca)" ... # 自定义 CA 证书文件
# 场景 8:PG + Redis 全外部
generate --domain=localhost --pg=external ... --pg-ssl=managed \
--redis=external --redis-url=redis://redis.example.com:6379
可以对照仓库中提交的两份典型期望输出:
- golden/local/docker-compose.yml:
nocodb+worker+db(postgres:17.10) +redis(redis:7) 四个服务,8080:8080端口映射,nocodb服务带GET /api/v1/health健康检查,worker通过depends_on: condition: service_healthy等待主服务; - golden/production-ssl/docker-compose.yml:在 nocodb 服务上追加
traefik.enable=true、Host(demo.example.com)路由规则、letsencrypt证书解析器四个 label,并新增完整的traefik:v3.6服务(80/443 入口、HTTP 强制跳转 HTTPS、ACME httpchallenge)。
3.2 密码归一化:唯一动态值的处理
内置 Postgres 的密码由 generate_password() 每次从 /dev/urandom 随机生成,若直接 diff 每次都会失败。helpers.bash 中的 normalize() 用 sed 在比对前把两处随机密码统一替换为占位符 __PASSWORD__:
normalize() {
LC_ALL=C sed -E \
-e 's/(POSTGRES_PASSWORD: ).*/\1__PASSWORD__/' \
-e 's/("password"[[:space:]]*:[[:space:]]*")[^"]*(")/\1__PASSWORD__\2/' \
"$1"
}
对应地,提交的黄金文件中密码位置就是字面量 __PASSWORD__(见 golden/local/docker-compose.yml 第 48 行的 POSTGRES_PASSWORD: __PASSWORD__)。快照测试失败时,bats 输出的是 diff -u 的 unified diff,能精确定位改动行。
3.3 除快照之外的产物断言
generate.bats 还包含一组针对性断言,覆盖安装器生成的其他文件与行为:
db.json内容:内置 PG 时client为pg、host为db(compose 服务名)、port为5432、database为nocodb,且不含ssl块;外部 PG 无 SSL 时host指向外部主机且无ssl块;外部 PG 自定义 CA 时db.json内嵌"rejectUnauthorized": true与BEGIN CERTIFICATE(证书内容被json_escape后逐字嵌入,对应 noco.sh 中generate_db_json()的 custom 分支)。- 密码一致性:内置 PG 场景下,从
docker-compose.yml的POSTGRES_PASSWORD与db.json的"password"提取的密码必须完全相同、长度 ≥ 24、且仅含字母数字——保证 compose 里的数据库与 NocoDB 的连接串不会因生成时序错位而失配。 docker.env固定配置块:NC_DB_JSON_FILE=/usr/app/data/db.json、NC_SECURE_ATTACHMENTS=true、NC_DISABLE_MUX=true必须逐行存在;NC_REDIS_URL在内置/外部 Redis 下分别取redis://redis:6379与给定 URL;NC_SITE_URL随模式变化——local 模式为http://localhost:8080、production-ip 为http://<ip>、production 为https://<domain>(对应generate_env()中 site_url 的三分支逻辑,该 URL 是邮件链接、Webhook 与 OAuth 回调正确解析所必需的)。- 安全与辅助文件:
docker.env权限必须为600(对应tighten_perms()的chmod 600,且脚本开头用umask 077从创建时就限制属主权限);update.sh必须可执行;.gitignore必须排除docker.env与nocodb/db.json两个含凭据的文件。 - 门控的 Docker 冒烟:
compose validates with docker compose config测试仅在TEST_DOCKER=1时运行,用真实的docker compose config校验生成物的 YAML 合法性,否则自动 skip。
四、validation.bats:非交互标志的 fail-fast 契约
validation.bats 守护的是运维契约:非交互模式下缺参必须立即以退出码 1 失败并打印可操作的错误信息,而不是挂起在提示符上。它通过 run_noco() 包装器(run env NOCO_SKIP_PREFLIGHT=1 bash noco.sh "$@")捕获退出码与输出,覆盖的失败用例包括:
| 用例 | 期望输出片段 |
|---|---|
缺 --pg |
--pg=bundled or --pg=external is required |
--pg=external 缺 --pg-host / --pg-user / --pg-password |
各自的 is required when --pg=external 提示 |
缺 --redis |
--redis=bundled or --redis=external is required |
--redis=external 缺 --redis-url |
--redis-url is required when --redis=external |
未知标志 --bogus |
Unknown flag |
production 域名缺 --acme-email |
--acme-email is required |
--pg-ssl=/no/such/ca.pem 指向不存在的 CA |
CA file not found |
--pg-ssl=mananged(拼写错误的值) |
同样报 CA file not found,而非静默写出空 CA |
已移除的子命令 upgrade |
提示 no longer a subcommand,引导使用 docker compose / update.sh 的新工作流 |
--help |
退出码 0 且打印 Non-interactive flags 用法 |
其中"拼写错误的 --pg-ssl 值必须失败"这条尤其值得注意:在 noco.sh 的 parse_flags() 中,--pg-ssl=managed 和 --pg-ssl=none 是精确分支,其余任何值都会落入 --pg-ssl=*) 分支被当作自定义 CA 路径处理,随后由 validate_non_interactive() 检查文件存在性——这个"兜底分支 + 文件存在校验"的组合把误拼写变成了显式错误,而不是生成一份缺失 CA 的坏配置。
五、examples.bats:安装器与文档示例的结构一致性
examples.bats 解决的是一个文档漂移问题:仓库在 docker-compose/examples/ 下提交了三份手写的部署示例(managed-postgres、postgres-private-ca、external-postgres-and-redis),它们各自带有一份 nocodb/db.json。这些示例与安装器实际生成的 db.json 描述的是同一批部署形态,一旦安装器生成逻辑变化而示例没更新,用户照着文档配置就会踩坑。
测试的比对方式是"结构同形"而非字节相同——用 jq 把所有字符串值抹平成常量 "X",只比较键结构与 ssl 块的形状:
norm_json() { jq -S 'walk(if type == "string" then "X" else . end)' "$1"; }
assert_same_structure() {
diff <(norm_json "$1") <(norm_json "$2")
}
三个用例分别把安装器生成的 db.json 与对应示例比对:
--pg-ssl=managed↔ examples/managed-postgres/nocodb/db.json;--pg-ssl="$(fake_ca)"(自定义 CA)↔ examples/postgres-private-ca/nocodb/db.json;--pg=external --redis=external↔ examples/external-postgres-and-redis/nocodb/db.json。
该文件在 jq 缺失时自动 skip,且文件头注释明确说明不要求字节一致——示例的 docker.env 合法地与安装器输出不同,只有 db.json 的结构需要保持一致。
六、interactive.bats:expect 驱动的向导 UX 守卫
interactive.bats 是唯一门控(gated)的用例:它不测标志驱动的生成路径,而是用 expects/install/interactive.sh 这份 expect 脚本真实驱动一遍交互式向导:
set env(PATH) "$here/../../mocks:$env(PATH)"
set env(NOCO_SKIP_PREFLIGHT) "1"
spawn bash "$here/../../../noco.sh"
expect "Domain or IP*" ; send "localhost\r" # 域名 → local 模式
expect ">*" ; send "1\r" # Postgres → 1) Bundled
expect ">*" ; send "1\r" # Redis → 1) Bundled
expect "Proceed?*" ; send "\r" # 确认摘要(默认 Y)
expect eof
两个细节保证它离线可跑:
- curl mock:安装器用
curl -s --max-time 3 https://api.ipify.org探测公网 IP 作为域名默认值,expect 脚本把mocks/目录前置到PATH,而 mocks/curl 只是一行exit 0的桩,从而切掉唯一的网络调用; - NOCO_SKIP_PREFLIGHT=1:跳过 Docker 检查与
docker compose up -d,expect 脚本只验证交互路径能把配置生成出来。
测试断言生成的 compose 中存在 '8080:8080' 端口映射、image: postgres 与 image: redis 两个内置服务。运行方式为:
TEST_INTERACTIVE=1 bats install/interactive.bats # 需要额外安装 expect
七、运行方式与依赖
在 docker-compose/1_Auto_Upstall/tests 目录下:
bats install/ # 运行整套快速测试
bats install/generate.bats # 只跑单个文件
TEST_DOCKER=1 bats install/generate.bats # 附加运行 `docker compose config` 校验
TEST_INTERACTIVE=1 bats install/interactive.bats # 运行 expect 驱动的向导测试
依赖:bats(整套)、jq(仅 examples.bats,缺失时 skip)、expect(仅门控的 interactive 测试,缺失时 skip)。由于全部生成运行都设置 NOCO_SKIP_PREFLIGHT=1 且网络调用被 mock,整套快速测试可在任意 OS、无 Docker、无网络的环境(包括 CI)中执行。
另外,helpers.bash 的 noco_scratch() 刻意不依赖 $BATS_TEST_TMPDIR——该变量自 bats 1.4.0 才存在,而 CI 所用发行版自带的 bats 1.2.1 下它是空值,因此辅助函数改用 mktemp -d 自建隔离目录并在 teardown 中清理。
八、黄金文件再生流程
当有意修改了 noco.sh 的 compose 生成逻辑(而非修复回归)时,测试不应被当作噪音绕过,而是走再生流程:
./lib/regen-golden.sh
git diff golden/ # 确认改动符合预期后再提交
regen-golden.sh 以与 generate.bats 完全相同的八组场景参数重跑安装器,把归一化(密码替换为 __PASSWORD__)后的 docker-compose.yml 覆盖写回 golden/<scenario>/;脚本头部的注释明确要求只在有意变更时运行,并且场景清单需与 generate.bats 保持同步。自定义 CA 场景使用运行时生成的临时 PEM 文件(与 helpers.bash 的 fake_ca() 内容一致),因为仓库 .gitignore 排除了 *.pem。
九、小结:这套测试设计的可复用点
从源码结构看,1_Auto_Upstall/tests/ 展示了一套针对"带系统副作用的生成型脚本"的可移植测试范式:
- 测试开关内建于被测脚本:
NOCO_SKIP_PREFLIGHT在check_prereqs/check_ports/start_stack三处短路,使生成逻辑与系统环境彻底解耦,测试矩阵因此可以在无 Docker、无网络的 CI 上全量运行; - 黄金文件 + 动态值归一化:唯一随机值(Postgres 密码)经 sed 归一为
__PASSWORD__后做diff -u快照,失败信息即精确 diff;再生流程(regen-golden.sh+git diff golden/审查)保证快照更新始终显式、可审计; - fail-fast 契约测试:把每条错误信息文案固化为断言,防止提示退化或误拼写参数静默产出坏配置(
--pg-ssl兜底分支的教训); - 结构一致性守卫:用
jq抹平字符串值后比对结构,让"安装器输出"与"文档示例"(docker-compose/examples/下三份db.json)在仓库内持续对齐,防止文档漂移; - 门控分层:
TEST_DOCKER=1、TEST_INTERACTIVE=1与jq/expect存在性检查让增强型测试按需启用,基础套件保持"零外部依赖 + 离线"的快速反馈。
对仓库使用者而言,该目录也是理解 noco.sh 行为契约的最佳入口:每个 @test 都精确对应安装器的一段生成或校验逻辑,配合 安装向导文档 可以完整掌握从交互提问到产物落盘的全过程。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00