package engine import ( "context" "errors" "os" "os/exec" "reflect" "testing" "time" ) type pointerOnlyLoop *pointerOnlyLoop type pointerOnlyConfig struct { Loop pointerOnlyLoop `yaml:"loop"` } func TestPointerOnlyDescriptorsTerminate(t *testing.T) { if mode := os.Getenv("CONFIGKIT_POINTER_DESCRIPTOR_TEST_MODE"); mode != "" { switch mode { case "decode": _, err := testLoad[pointerOnlyConfig](Bytes("config.yml", []byte("loop: {}\n"))) var loadErr *LoadError if !errors.As(err, &loadErr) { t.Fatalf("load error = %v, want a safe decode failure", err) } for _, diagnostic := range loadErr.Diagnostics() { if diagnostic.Code == CodeDecodeType && diagnostic.Path.String() == "/loop" { return } } t.Fatalf("diagnostics = %#v, want decode type at /loop", loadErr.Diagnostics()) case "paths": schema, issue := testBuildSchema(reflect.TypeFor[pointerOnlyConfig]()) if issue != nil { t.Fatalf("schema issue = %#v", issue) } if _, ok := resolveSchemaPath(schema, Keys("loop", "child")); ok { t.Fatal("descendant below a pointer-only loop was accepted") } if _, ok := schemaSecretRoot(schema, Keys("loop", "child")); ok { t.Fatal("secret path below a pointer-only loop was accepted") } if acceptsMapping(schema.root.fields[schema.root.fieldByName["loop"]].descriptor) { t.Fatal("pointer-only loop was accepted as a mapping") } default: t.Fatalf("unknown test mode %q", mode) } return } for _, mode := range []string{"decode", "paths"} { t.Run(mode, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestPointerOnlyDescriptorsTerminate$") command.Env = append(os.Environ(), "CONFIGKIT_POINTER_DESCRIPTOR_TEST_MODE="+mode) output, err := command.CombinedOutput() if ctx.Err() != nil { t.Fatalf("pointer descriptor %s did not terminate: %s", mode, output) } if err != nil { t.Fatalf("pointer descriptor %s failed: %v, %s", mode, err, output) } }) } } func TestPointerOnlyNullableFieldAndRecursiveValues(t *testing.T) { for name, source := range map[string]Source{ "omitted": NoSource(), "null": Bytes("config.yml", []byte("loop: null\n")), } { t.Run(name, func(t *testing.T) { result, err := testLoad[pointerOnlyConfig](source) if err != nil || result.Config.Loop != nil { t.Fatalf("result = %#v, error = %v", result, err) } }) } result, err := testLoad[recursiveConfig](Bytes("config.yml", []byte("name: root\nnext:\n name: child\n next:\n name: leaf\n"))) if err != nil { t.Fatal(err) } if result.Config.Name != "root" || result.Config.Next == nil || result.Config.Next.Name != "child" || result.Config.Next.Next == nil || result.Config.Next.Next.Name != "leaf" { t.Fatalf("recursive value = %#v", result.Config) } schema, issue := testBuildSchema(reflect.TypeFor[pointerOnlyConfig]()) if issue != nil { t.Fatal(issue.message) } resolved, ok := resolveSchemaPath(schema, Keys("loop")) if !ok || resolved.descriptor.kind != descriptorPointer { t.Fatalf("nullable pointer field resolution = %#v, %v", resolved, ok) } called := false nullableResult, err := testLoad[pointerOnlyConfig](NoSource(), Options[pointerOnlyConfig]{Definition: Definition[pointerOnlyConfig]{ Resolve: func(config *pointerOnlyConfig, resolution *Resolution) error { called = true Normalize(resolution, Keys("loop"), &config.Loop, nil, "clear nullable field") return nil }, }}) if err != nil || !called || nullableResult.Config.Loop != nil { t.Fatalf("nullable field operation = %#v, called=%v, error=%v", nullableResult, called, err) } }