// dependency_test.go - 依赖解析器的单元测试. // // 覆盖: // - ResolveClosure: 无依赖,线性链,钻石形,环,缺失依赖,跨来源阻断 // - VerifyAndDemote: 传播禁用,固定点收敛,多轮降级 // - FindReverseDependents package plugin import ( "fmt" "testing" ) // makePlugin 快捷构造测试用 Plugin,deps 是其依赖列表. func makePlugin(name string, src Source, deps ...string) *Plugin { return &Plugin{ Name: name, Source: src, Enabled: true, Hooks: make(map[string][]HookDef), Manifest: &Manifest{ Name: name, Dependencies: deps, }, } } // makeRegistry 将插件列表转为 name→*Plugin map. func makeRegistry(plugins ...*Plugin) map[string]*Plugin { m := make(map[string]*Plugin, len(plugins)) for _, p := range plugins { m[p.Name] = p } return m } // ── ResolveClosure ──────────────────────────────────────────────────────────── // TestResolveClosure_NoDeps 单个无依赖插件,直接返回自身. func TestResolveClosure_NoDeps(t *testing.T) { a := makePlugin("a", SourceUser) reg := makeRegistry(a) ordered, errs := ResolveClosure([]string{"a"}, reg) if len(errs) != 0 { t.Fatalf("无依赖不应有错误: %v", errs) } if len(ordered) != 1 || ordered[0].Name != "a" { t.Errorf("ordered = %v, 期望 [a]", names(ordered)) } } // TestResolveClosure_LinearChain a→b→c,结果应为 [c, b, a](依赖先) func TestResolveClosure_LinearChain(t *testing.T) { a := makePlugin("a", SourceUser, "b") b := makePlugin("b", SourceUser, "c") c := makePlugin("c", SourceUser) reg := makeRegistry(a, b, c) ordered, errs := ResolveClosure([]string{"a"}, reg) if len(errs) != 0 { t.Fatalf("线性链无环不应有错误: %v", errs) } // 拓扑顺序:c 先于 b 先于 a pos := func(name string) int { for i, p := range ordered { if p.Name == name { return i } } return -1 } if pos("c") > pos("b") || pos("b") > pos("a") { t.Errorf("拓扑顺序错误: %v", names(ordered)) } } // TestResolveClosure_Diamond a→b, a→c, b→d, c→d - d 只出现一次 func TestResolveClosure_Diamond(t *testing.T) { a := makePlugin("a", SourceUser, "b", "c") b := makePlugin("b", SourceUser, "d") c := makePlugin("c", SourceUser, "d") d := makePlugin("d", SourceUser) reg := makeRegistry(a, b, c, d) ordered, errs := ResolveClosure([]string{"a"}, reg) if len(errs) != 0 { t.Fatalf("钻石图无环不应有错误: %v", errs) } // d 只应出现一次 count := 0 for _, p := range ordered { if p.Name == "d" { count++ } } if count != 1 { t.Errorf("d 应只出现一次,实际 %d 次", count) } } // TestResolveClosure_Cycle a→b→a 检测环 func TestResolveClosure_Cycle(t *testing.T) { a := makePlugin("a", SourceUser, "b") b := makePlugin("b", SourceUser, "a") reg := makeRegistry(a, b) _, errs := ResolveClosure([]string{"a"}, reg) if len(errs) == 0 { t.Fatal("环依赖应返回错误") } hasCycleErr := false for _, e := range errs { if e.Code == ErrDependencyCycle { hasCycleErr = true } } if !hasCycleErr { t.Errorf("应包含 ErrDependencyCycle, 实际: %v", errs) } } // TestResolveClosure_MissingDep 依赖不在注册表中 func TestResolveClosure_MissingDep(t *testing.T) { a := makePlugin("a", SourceUser, "missing-plugin") reg := makeRegistry(a) _, errs := ResolveClosure([]string{"a"}, reg) if len(errs) == 0 { t.Fatal("依赖缺失应返回错误") } hasMissingErr := false for _, e := range errs { if e.Code == ErrDependencyMissing { hasMissingErr = true } } if !hasMissingErr { t.Errorf("应包含 ErrDependencyMissing, 实际: %v", errs) } } // TestResolveClosure_CrossSource 项目级插件依赖用户级插件 - 应报错 func TestResolveClosure_CrossSource(t *testing.T) { proj := makePlugin("proj-plugin", SourceProject, "user-lib") userLib := makePlugin("user-lib", SourceUser) reg := makeRegistry(proj, userLib) _, errs := ResolveClosure([]string{"proj-plugin"}, reg) if len(errs) == 0 { t.Fatal("跨来源依赖应返回错误") } hasCrossErr := false for _, e := range errs { if e.Code == ErrDependencyCrossSource { hasCrossErr = true } } if !hasCrossErr { t.Errorf("应包含 ErrDependencyCrossSource, 实际: %v", errs) } } // TestResolveClosure_BuiltinDepAllowed 任何来源依赖内置插件均合法 func TestResolveClosure_BuiltinDepAllowed(t *testing.T) { proj := makePlugin("proj-plugin", SourceProject, "builtin-lib") builtin := makePlugin("builtin-lib", SourceBuiltin) reg := makeRegistry(proj, builtin) _, errs := ResolveClosure([]string{"proj-plugin"}, reg) // 内置插件不应触发跨来源错误 for _, e := range errs { if e.Code == ErrDependencyCrossSource { t.Errorf("依赖内置插件不应报跨来源错误: %v", e) } } } // TestResolveClosure_EmptyNames 空请求列表,返回空结果 func TestResolveClosure_EmptyNames(t *testing.T) { ordered, errs := ResolveClosure([]string{}, map[string]*Plugin{}) if len(ordered) != 0 || len(errs) != 0 { t.Errorf("空请求应返回空结果") } } // ── VerifyAndDemote ─────────────────────────────────────────────────────────── // TestVerifyAndDemote_NoDeps 无依赖,全部保持启用 func TestVerifyAndDemote_NoDeps(t *testing.T) { a := makePlugin("a", SourceUser) b := makePlugin("b", SourceUser) enabled, disabled, warnings := VerifyAndDemote([]*Plugin{a, b}, nil, nil) if len(enabled) != 2 { t.Errorf("无依赖应全部启用,实际 enabled=%d", len(enabled)) } if len(disabled) != 0 { t.Errorf("无应被禁用,实际 disabled=%d", len(disabled)) } if len(warnings) != 0 { t.Errorf("无警告,实际 warnings=%d", len(warnings)) } } // TestVerifyAndDemote_DepInDisabled b 依赖 a,a 已在 disabled 列表 → b 也应降级 func TestVerifyAndDemote_DepInDisabled(t *testing.T) { a := makePlugin("a", SourceUser) b := makePlugin("b", SourceUser, "a") enabled, disabled, warnings := VerifyAndDemote([]*Plugin{b}, []*Plugin{a}, nil) if len(enabled) != 0 { t.Errorf("b 的依赖 a 被禁用,b 应降级,但 enabled=%d", len(enabled)) } if len(disabled) != 1 || disabled[0].Name != "b" { t.Errorf("b 应在 disabled 中: %v", names(disabled)) } if len(warnings) == 0 { t.Error("降级应产生 warning") } } // TestVerifyAndDemote_DepInErrors b 依赖 a,a 有加载错误 → b 也应降级 func TestVerifyAndDemote_DepInErrors(t *testing.T) { b := makePlugin("b", SourceUser, "a") loadErrs := []PluginError{{ Code: ErrManifestNotFound, PluginName: "a", Message: "manifest not found", }} enabled, _, _ := VerifyAndDemote([]*Plugin{b}, nil, loadErrs) if len(enabled) != 0 { t.Errorf("b 的依赖 a 有错误,b 应降级") } } // TestVerifyAndDemote_ChainPropagation a→b→c,a 被禁用,b 和 c 都应被传递降级 func TestVerifyAndDemote_ChainPropagation(t *testing.T) { a := makePlugin("a", SourceUser) // 已在 disabled b := makePlugin("b", SourceUser, "a") c := makePlugin("c", SourceUser, "b") enabled, disabled, _ := VerifyAndDemote([]*Plugin{b, c}, []*Plugin{a}, nil) if len(enabled) != 0 { t.Errorf("链式传播后应全部降级, enabled=%v", names(enabled)) } disabledNames := make(map[string]bool) for _, p := range disabled { disabledNames[p.Name] = true } if !disabledNames["b"] || !disabledNames["c"] { t.Errorf("b 和 c 都应被降级, disabled=%v", names(disabled)) } } // TestVerifyAndDemote_IndependentUnaffected a 被禁用,与 a 无关的 x 不受影响 func TestVerifyAndDemote_IndependentUnaffected(t *testing.T) { a := makePlugin("a", SourceUser) x := makePlugin("x", SourceUser) // 无依赖,与 a 无关 enabled, disabled, _ := VerifyAndDemote([]*Plugin{x}, []*Plugin{a}, nil) if len(enabled) != 1 || enabled[0].Name != "x" { t.Errorf("x 与 a 无关,应保持启用, enabled=%v", names(enabled)) } if len(disabled) != 0 { t.Errorf("x 不应被降级, disabled=%v", names(disabled)) } } // ── FindReverseDependents ───────────────────────────────────────────────────── func TestFindReverseDependents_Basic(t *testing.T) { a := makePlugin("a", SourceUser) b := makePlugin("b", SourceUser, "a") c := makePlugin("c", SourceUser, "a") d := makePlugin("d", SourceUser, "x") // 不依赖 a revDeps := FindReverseDependents("a", []*Plugin{a, b, c, d}) if len(revDeps) != 2 { t.Errorf("a 应有 2 个反向依赖, 实际: %v", revDeps) } revSet := make(map[string]bool) for _, name := range revDeps { revSet[name] = true } if !revSet["b"] || !revSet["c"] { t.Errorf("b 和 c 应在反向依赖中, 实际: %v", revDeps) } } func TestFindReverseDependents_None(t *testing.T) { a := makePlugin("a", SourceUser) b := makePlugin("b", SourceUser) revDeps := FindReverseDependents("a", []*Plugin{a, b}) if len(revDeps) != 0 { t.Errorf("无反向依赖应返回空, 实际: %v", revDeps) } } func TestFindReverseDependents_NilManifest(t *testing.T) { // 内置插件 Manifest 为 nil,不应 panic builtin := &Plugin{Name: "builtin", Source: SourceBuiltin} revDeps := FindReverseDependents("x", []*Plugin{builtin}) if len(revDeps) != 0 { t.Errorf("nil manifest 不应 panic,也不应有结果: %v", revDeps) } } // ── PluginError ─────────────────────────────────────────────────────────────── func TestPluginError_Error_WithCause(t *testing.T) { e := PluginError{ Code: ErrManifestNotFound, PluginName: "my-plugin", Message: "file not found", Cause: fmt.Errorf("no such file"), } got := e.Error() if got == "" { t.Error("Error() 不应为空") } // 应包含插件名,错误码名称和原因 for _, want := range []string{"my-plugin", "manifest_not_found", "no such file"} { if !contains(got, want) { t.Errorf("Error() = %q, 应包含 %q", got, want) } } } func TestPluginError_Error_NoCause(t *testing.T) { e := PluginError{ Code: ErrDependencyCycle, PluginName: "p", Message: "cycle detected", } got := e.Error() if contains(got, "") { t.Errorf("无 Cause 不应包含 : %q", got) } } func TestLoadResult_Summary(t *testing.T) { r := &LoadResult{ Enabled: []*Plugin{{Name: "a"}, {Name: "b"}}, Disabled: []*Plugin{{Name: "c"}}, Errors: []PluginError{{Code: ErrManifestNotFound}}, } s := r.Summary() if s == "" { t.Error("Summary() 不应为空") } for _, want := range []string{"2", "1"} { if !contains(s, want) { t.Errorf("Summary() = %q, 应包含数字 %q", s, want) } } } func TestLoadResult_HasErrors(t *testing.T) { r := &LoadResult{} if r.HasErrors() { t.Error("空结果不应 HasErrors") } r.Errors = append(r.Errors, PluginError{Code: ErrManifestInvalid}) if !r.HasErrors() { t.Error("有错误应 HasErrors") } } // ── 辅助函数 ────────────────────────────────────────────────────────────────── // names 将插件列表转为名称列表,方便在错误消息中打印. func names(plugins []*Plugin) []string { s := make([]string, len(plugins)) for i, p := range plugins { s[i] = p.Name } return s } // contains 检查字符串 s 中是否包含子串 sub. func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexStr(s, sub) >= 0) } // indexStr 简单的子串搜索(不引入 strings 包以保持独立性). func indexStr(s, sub string) int { if len(sub) == 0 { return 0 } for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }