From fe883d5362d9ae7d775fba31cd2d8aa4bd0a5bac Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 18:38:05 +0800 Subject: [PATCH] =?UTF-8?q?test(plugins):=20=E7=9C=9F=E5=AE=9E=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E4=BA=A7=E7=89=A9=E5=85=88=E6=A0=A1=E9=AA=8C=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=86=8D=20exec=EF=BC=8C=E9=94=99=E8=AF=AF=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=BB=99=E5=8F=AF=E8=AF=BB=E6=8F=90=E7=A4=BA=E8=80=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=20exec=20format=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排查知识库改动是否引入回归时,internal/plugins 6 个测试全红、报 "fork/exec .../plugin.bin: exec format error"。查了半小时才发现与代码无关: 早前验证跨平台示例构建时,最后一次构建(darwin/arm64)把 `example/*/build/plugin.bin` 覆盖成了 Mach-O arm64,而 realPluginBinary 只按文件名找候选、**不校验平台**,于是拿 macOS 产物在本机 exec。 两个陷阱一起堵: - 校验魔数(ELF / Mach-O / PE),平台不符则 **SKIP 并给出可直接粘贴的重建命令** (`hmapdev build --target --no-bundle`),不再用误导性的 exec format error; - 明确写出「产物缺失即 skip」的语义,避免"全绿其实什么都没验" (本次对照实验里,改动前的 worktree 因无产物而全绿,看着像通过)。 反向验证:把 weather 产物换成 PE 假头 → 相关测试转为 SKIP 且提示平台与重建命令 ✓。 --- internal/plugins/real_plugin_smoke_test.go | 51 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/plugins/real_plugin_smoke_test.go b/internal/plugins/real_plugin_smoke_test.go index 9633b7d..6e64570 100644 --- a/internal/plugins/real_plugin_smoke_test.go +++ b/internal/plugins/real_plugin_smoke_test.go @@ -3,6 +3,7 @@ package plugins import ( + "bytes" "fmt" "os" "os/exec" @@ -26,6 +27,47 @@ import ( // 前置:插件需已用新版 hmapdev 重编(scripts/rebuild-plugins.sh)。 // 未重编时测试 skip 而非 fail——CI 上不强制要求先跑重编脚本。 +// hostExecutableKind 按魔数判断产物是不是**本机**可执行文件,返回可读格式名。 +// +// 为何需要:以前只按文件名找候选(plugin.bin__ → plugin.bin),拿到 +// darwin/windows 产物就直接 exec,报的是 "exec format error"——看起来像插件坏了, +// 实际上只是**开发环境里的产物平台不对**。本轮就踩了:跨平台示例构建把 +// build/plugin.bin 覆盖成 Mach-O arm64,internal/plugins 6 个测试全红,排查半小时。 +func hostExecutableKind(path string) (string, bool) { + f, err := os.Open(path) + if err != nil { + return "无法读取", false + } + defer f.Close() + head := make([]byte, 4) + n, _ := f.Read(head) + if n < 2 { + return "文件过短", false + } + switch runtime.GOOS { + case "linux": + if n >= 4 && head[0] == 0x7f && head[1] == 'E' && head[2] == 'L' && head[3] == 'F' { + return "ELF(本机)", true + } + if n >= 4 && head[0] == 0xcf && head[1] == 0xfa && head[2] == 0xed && head[3] == 0xfe { + return "Mach-O 64 little-endian(macOS,不是本机)", false + } + if n >= 2 && head[0] == 'M' && head[1] == 'Z' { + return "PE(Windows,不是本机)", false + } + return "未知格式(不是本机可执行文件)", false + case "darwin": + for _, m := range [][]byte{{0xcf, 0xfa, 0xed, 0xfe}, {0xca, 0xfe, 0xba, 0xbe}} { + if n >= 4 && bytes.Equal(head, m) { + return "Mach-O(本机)", true + } + } + return "不是本机可执行文件", false + default: + return "未知平台(默认放行)", true + } +} + // realPluginDir 返回某个 example 插件的 linux 产物路径。 func realPluginBinary(t *testing.T, name string) string { t.Helper() @@ -39,9 +81,14 @@ func realPluginBinary(t *testing.T, name string) string { filepath.Join(root, "build", "plugin.bin"), } for _, c := range candidates { - if st, err := os.Stat(c); err == nil && !st.IsDir() { - return c + if st, err := os.Stat(c); err != nil || st.IsDir() { + continue } + if kind, ok := hostExecutableKind(c); !ok { + t.Skipf("插件 %s 的产物 %s 不是本机可执行格式(%s);重建:cd %s && hmapdev build --target %s/%s --no-bundle", + name, c, kind, root, runtime.GOOS, runtime.GOARCH) + } + return c } t.Skipf("插件 %s 未重编(先跑 scripts/rebuild-plugins.sh)", name) return ""