Files
unkin-agent 2e2562018f index every name in an include list
include/contain/require take a list of names; parse the argument list so
the second and later names are indexed too.
2026-09-12 16:46:51 +10:00

157 lines
5.5 KiB
Go

package impact
import (
"reflect"
"testing"
)
func testIndex(t *testing.T) *Index {
t.Helper()
idx, err := BuildIndex("testdata/repo")
if err != nil {
t.Fatalf("BuildIndex: %v", err)
}
return idx
}
func resolvedHosts(t *testing.T, class string) []string {
t.Helper()
res := newClassResolver(testIndex(t), fixtureNodes()).resolve(class)
hosts := make([]string, 0, len(res.hosts))
for certname := range res.hosts {
hosts = append(hosts, certname)
}
return uniqueSorted(hosts)
}
func TestResolveThroughIncludeChain(t *testing.T) {
// profiles::leaf <- profiles::mid <- profiles::base <- two role classes.
if got, want := resolvedHosts(t, "profiles::leaf"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("profiles::leaf = %v, want %v", got, want)
}
}
func TestResolveContainAndClassResource(t *testing.T) {
for _, class := range []string{"profiles::contained", "profiles::viaclass"} {
if got, want := resolvedHosts(t, class), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("%s = %v, want %v", class, got, want)
}
}
}
func TestResolveDefinedTypeUsage(t *testing.T) {
if got, want := resolvedHosts(t, "profiles::defined::thing"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("profiles::defined::thing = %v, want %v", got, want)
}
}
func TestResolveTerminatesOnCycle(t *testing.T) {
// profiles::cycle::a and ::b include each other; ::a is pulled in by
// profiles::base, so the walk must reach the roles and stop.
if got, want := resolvedHosts(t, "profiles::cycle::b"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("profiles::cycle::b = %v, want %v", got, want)
}
}
func TestResolveViaHieraIncludeOnly(t *testing.T) {
// The jellyfin module is applied by hiera_include in a role yaml and by
// nothing in any manifest — a .pp-only search would call it orphaned.
if got, want := resolvedHosts(t, "jellyfin"), []string{"web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("jellyfin = %v, want %v", got, want)
}
// One hop further in: jellyfin::install is only reachable through jellyfin.
if got, want := resolvedHosts(t, "jellyfin::install"), []string{"web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("jellyfin::install = %v, want %v", got, want)
}
}
func TestResolveHieraIncludeFromVirtualHierarchy(t *testing.T) {
if got, want := resolvedHosts(t, "profiles::lldpd"), []string{"k8s1", "k8s2", "prox1"}; !reflect.DeepEqual(got, want) {
t.Errorf("profiles::lldpd = %v, want %v", got, want)
}
}
func TestResolveHonoursHieraExclude(t *testing.T) {
// common.yaml pulls basemod in for everyone; roles/infra/proxmox.yaml
// excludes it again, so the proxmox node drops out.
if got, want := resolvedHosts(t, "basemod"), []string{"bare1", "k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
t.Errorf("basemod = %v, want %v", got, want)
}
}
func TestResolveOrphanClass(t *testing.T) {
res := newClassResolver(testIndex(t), fixtureNodes()).resolve("orphanmod")
if !res.orphan {
t.Errorf("orphanmod should be orphaned, hosts = %v", res.hosts)
}
if len(res.hosts) != 0 {
t.Errorf("orphanmod reached %v", res.hosts)
}
}
func TestIndexIgnoresCommentedReferences(t *testing.T) {
if got := testIndex(t).IncludedBy("profiles::commented"); len(got) != 0 {
t.Errorf("a commented-out class name was indexed as a reference: %v", got)
}
}
func TestIndexAssetOwners(t *testing.T) {
idx := testIndex(t)
for _, asset := range []string{"profiles/leaf/x.erb", "profiles/leaf/y.txt"} {
if got, want := idx.AssetOwners(asset), []string{"profiles::leaf"}; !reflect.DeepEqual(got, want) {
t.Errorf("AssetOwners(%q) = %v, want %v", asset, got, want)
}
}
}
func TestIndexModuleClasses(t *testing.T) {
got := testIndex(t).ModuleClasses("jellyfin")
want := []string{"jellyfin", "jellyfin::init", "jellyfin::install"}
if !reflect.DeepEqual(got, want) {
t.Errorf("ModuleClasses(jellyfin) = %v, want %v", got, want)
}
}
func TestParseHieraListsRegexFallback(t *testing.T) {
// eyaml payloads a strict parser rejects still have to yield their lists.
broken := []byte("---\nhiera_include:\n - alpha\n - beta\nhiera_exclude:\n - gamma\nbad: [unclosed\n")
include, exclude := parseHieraLists(broken)
if !reflect.DeepEqual(include, []string{"alpha", "beta"}) {
t.Errorf("include = %v", include)
}
if !reflect.DeepEqual(exclude, []string{"gamma"}) {
t.Errorf("exclude = %v", exclude)
}
}
func TestResolveInheritsScopeVarCreateResourcesAndInlineDefine(t *testing.T) {
// Four reference forms a plain include grep misses: class inheritance,
// a $class::var read, create_resources() naming a type in a string, and a
// defined type declared part-way along a line.
tests := map[string]string{
"inherits": "profiles::inherited",
"scope variable": "profiles::scopevar",
"create_resources": "profiles::created",
"inline define": "profiles::inline::thing",
}
want := []string{"k8s1", "k8s2", "web1"}
for name, class := range tests {
t.Run(name, func(t *testing.T) {
if got := resolvedHosts(t, class); !reflect.DeepEqual(got, want) {
t.Errorf("%s = %v, want %v", class, got, want)
}
})
}
}
func TestResolveEveryNameInAnIncludeList(t *testing.T) {
// profiles::base pulls both classes in with one comma-separated include;
// the second name must not be dropped.
want := []string{"k8s1", "k8s2", "web1"}
for _, class := range []string{"profiles::listed::a", "profiles::listed::b"} {
if got := resolvedHosts(t, class); !reflect.DeepEqual(got, want) {
t.Errorf("%s = %v, want %v", class, got, want)
}
}
}