package hcllang import ( "io" "testing" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" ffs "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/helmexec" ) func newHCLLoader() *HCLLoader { log := helmexec.NewLogger(io.Discard, "debug") return &HCLLoader{ fs: ffs.DefaultFileSystem(), logger: log, } } func TestHCL_localsTraversalsParser(t *testing.T) { l := newHCLLoader() files := []string{"testdata/values.1.hcl"} l.AddFiles(files) _, filesLocals, diags := l.readHCLs() if diags != nil { t.Errorf("Test file parsing error : %s", diags.Errs()[0].Error()) } actual := make(map[string]map[string]int) for file, locals := range filesLocals { actual[file] = make(map[string]int) for k, v := range locals { actual[file][k] = len(v.Expr.Variables()) } } expected := map[string]map[string]int{ "testdata/values.1.hcl": { "myLocal": 0, "myLocalRef": 1, }, } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } } func TestHCL_localsTraversalsAttrParser(t *testing.T) { l := newHCLLoader() files := []string{"testdata/values.1.hcl"} l.AddFiles(files) _, filesLocals, diags := l.readHCLs() if diags != nil { t.Errorf("Test file parsing error : %s", diags.Errs()[0].Error()) } actual := make(map[string]map[string]string) for file, locals := range filesLocals { actual[file] = make(map[string]string) for k, v := range locals { str := "" for _, v := range v.Expr.Variables() { tr, _ := l.parseSingleAttrRef(v, LocalsBlockIdentifier) str += tr } actual[file][k] = str } } expected := map[string]map[string]string{ "testdata/values.1.hcl": { "myLocal": "", "myLocalRef": "myLocal", }, } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } } func TestHCL_valuesTraversalsParser(t *testing.T) { l := newHCLLoader() files := []string{"testdata/values.1.hcl"} l.AddFiles(files) fileValues, _, diags := l.readHCLs() if diags != nil { t.Errorf("Test file parsing error : %s", diags.Errs()[0].Error()) } actual := make(map[string]int) for k, v := range fileValues { actual[k] = len(v.Expr.Variables()) } expected := map[string]int{ "val1": 0, "val2": 1, "val3": 2, "val4": 1, } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } } func TestHCL_valuesTraversalsAttrParser(t *testing.T) { l := newHCLLoader() files := []string{"testdata/values.1.hcl"} l.AddFiles(files) fileValues, _, diags := l.readHCLs() if diags != nil { t.Errorf("Test file parsing error : %s", diags.Errs()[0].Error()) } actual := make(map[string]string) for k, v := range fileValues { str := "" for _, v := range v.Expr.Variables() { tr, _ := l.parseSingleAttrRef(v, ValuesBlockIdentifier) str += tr } actual[k] = str } expected := map[string]string{ "val1": "", "val2": "", "val3": "val1", "val4": "val1", } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } } func TestHCL_resultValidate(t *testing.T) { l := newHCLLoader() files := []string{"testdata/values.1.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Errorf("Render error : %s", err.Error()) } expected := map[string]any{ "val1": float64(1), "val2": "LOCAL", "val3": "local1", "val4": float64(-1), } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } } func TestCtyMergeValues_SimpleTypes(t *testing.T) { tests := []struct { name string a cty.Value b cty.Value expected cty.Value }{ { name: "merge strings - b wins", a: cty.StringVal("original"), b: cty.StringVal("override"), expected: cty.StringVal("override"), }, { name: "merge numbers - b wins", a: cty.NumberIntVal(42), b: cty.NumberIntVal(100), expected: cty.NumberIntVal(100), }, { name: "merge bools - b wins", a: cty.BoolVal(true), b: cty.BoolVal(false), expected: cty.BoolVal(false), }, { name: "b is null - keep a", a: cty.StringVal("keep"), b: cty.NullVal(cty.String), expected: cty.StringVal("keep"), }, { name: "a is null - use b", a: cty.NullVal(cty.String), b: cty.StringVal("new"), expected: cty.StringVal("new"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ctyMergeValues(tt.a, tt.b) if !result.RawEquals(tt.expected) { t.Errorf("ctyMergeValues() = %v, want %v", result, tt.expected) } }) } } func TestCtyMergeValues_Objects(t *testing.T) { tests := []struct { name string a cty.Value b cty.Value expected cty.Value }{ { name: "merge objects - shallow override", a: cty.ObjectVal(map[string]cty.Value{ "key1": cty.StringVal("value1"), "key2": cty.StringVal("value2"), }), b: cty.ObjectVal(map[string]cty.Value{ "key2": cty.StringVal("overridden"), "key3": cty.StringVal("new"), }), expected: cty.ObjectVal(map[string]cty.Value{ "key1": cty.StringVal("value1"), "key2": cty.StringVal("overridden"), "key3": cty.StringVal("new"), }), }, { name: "merge objects - nested merge", a: cty.ObjectVal(map[string]cty.Value{ "parent": cty.ObjectVal(map[string]cty.Value{ "child1": cty.StringVal("original"), "child2": cty.StringVal("keep"), }), }), b: cty.ObjectVal(map[string]cty.Value{ "parent": cty.ObjectVal(map[string]cty.Value{ "child1": cty.StringVal("override"), "child3": cty.StringVal("new"), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "parent": cty.ObjectVal(map[string]cty.Value{ "child1": cty.StringVal("override"), "child2": cty.StringVal("keep"), "child3": cty.StringVal("new"), }), }), }, { name: "merge objects - new keys in b", a: cty.ObjectVal(map[string]cty.Value{ "existing": cty.StringVal("value"), }), b: cty.ObjectVal(map[string]cty.Value{ "new1": cty.StringVal("newvalue1"), "new2": cty.NumberIntVal(42), }), expected: cty.ObjectVal(map[string]cty.Value{ "existing": cty.StringVal("value"), "new1": cty.StringVal("newvalue1"), "new2": cty.NumberIntVal(42), }), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ctyMergeValues(tt.a, tt.b) if !result.RawEquals(tt.expected) { t.Errorf("ctyMergeValues() = %#v, want %#v", result, tt.expected) } }) } } func TestCtyMergeValues_Lists(t *testing.T) { tests := []struct { name string a cty.Value b cty.Value expected cty.Value }{ { name: "merge lists - b replaces a", a: cty.ListVal([]cty.Value{ cty.StringVal("a"), cty.StringVal("b"), }), b: cty.ListVal([]cty.Value{ cty.StringVal("x"), cty.StringVal("y"), cty.StringVal("z"), }), expected: cty.ListVal([]cty.Value{ cty.StringVal("x"), cty.StringVal("y"), cty.StringVal("z"), }), }, { name: "merge tuples - b replaces a", a: cty.TupleVal([]cty.Value{ cty.StringVal("a"), cty.NumberIntVal(1), }), b: cty.TupleVal([]cty.Value{ cty.StringVal("x"), cty.NumberIntVal(99), }), expected: cty.TupleVal([]cty.Value{ cty.StringVal("x"), cty.NumberIntVal(99), }), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ctyMergeValues(tt.a, tt.b) if !result.RawEquals(tt.expected) { t.Errorf("ctyMergeValues() = %v, want %v", result, tt.expected) } }) } } func TestHCL_ValuesOverride(t *testing.T) { l := newHCLLoader() files := []string{"testdata/override.1.hcl", "testdata/override.2.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error: %s", err.Error()) } // Verify that values from file2 override file1 if actual["env"] != "prod" { t.Errorf("Expected env=prod (from override), got %v", actual["env"]) } // Verify nested object merge config, ok := actual["config"].(map[string]any) if !ok { t.Fatalf("Expected config to be a map, got %T", actual["config"]) } if config["replicas"] != float64(3) { t.Errorf("Expected replicas=3 (overridden), got %v", config["replicas"]) } if config["image"] != "v1.1" { t.Errorf("Expected image=v1.1 (overridden), got %v", config["image"]) } if config["debug"] != true { t.Errorf("Expected debug=true (new from file2), got %v", config["debug"]) } // Verify list from file1 is preserved (file2 doesn't define tags) tags, ok := actual["tags"].([]any) if !ok || len(tags) != 2 { t.Errorf("Expected tags to be preserved from file1, got %v", actual["tags"]) } // Verify no override with null value if actual["class"] != "standard" { t.Errorf("Expected class=standard (preserved from file1), got %v", actual["class"]) } // Verify new key from file2, using hv accessor if actual["region"] != "us-east" { t.Errorf("Expected region=us-east (new in file2), got %v", actual["region"]) } // Verify new uppered key from file2 if actual["status"] != "READY" { t.Errorf("Expected status=READY (new in file2), got %v", actual["status"]) } // Verify map merge annotations, ok := actual["annotations"].(map[string]any) if !ok { t.Fatalf("Expected annotations to be a map, got %T", actual["annotations"]) } if annotations["a"] != "val2" { t.Errorf("Expected a=val2 (overridden), got %v", annotations["a"]) } if annotations["b"] != "val3" { t.Errorf("Expected b=val3 (overridden), got %v", annotations["b"]) } // Verify mixed-types merge mixed_types, ok := actual["mixed_types"].(map[string]any) if !ok { t.Fatalf("Expected mixed_types to be a map, got %T", actual["mixed_types"]) } if mixed_types["string_value"] != false { t.Errorf("Expected string_value=false (overridden), got %v", mixed_types["string_value"]) } number_value, ok := mixed_types["number_value"].([]any) if !ok { t.Fatalf("Expected number_value to be a slice, got %T", mixed_types["number_value"]) } if number_value[0] != "val1" { t.Errorf("Expected number_value[0]='val1' (overridden), got %v", number_value[0]) } if number_value[1] != "val2" { t.Errorf("Expected number_value[1]='val2' (overridden), got %v", number_value[1]) } if mixed_types["bool_value"] != float64(1) { t.Errorf("Expected bool_value=1 (overridden), got %v", mixed_types["bool_value"]) } if mixed_types["list_value"] != "item1" { t.Errorf("Expected list_value='item1' (overridden), got %v", mixed_types["list_value"]) } } func TestHCL_ValuesOverride_ThreeFiles(t *testing.T) { l := newHCLLoader() files := []string{"testdata/multi.1.hcl", "testdata/multi.2.hcl", "testdata/multi.3.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error: %s", err.Error()) } // Last file wins for simple values if actual["base"] != "file3" { t.Errorf("Expected base=file3 (last override), got %v", actual["base"]) } // Check deep merge of shared object shared, ok := actual["shared"].(map[string]any) if !ok { t.Fatalf("Expected shared to be a map, got %T", actual["shared"]) } if shared["key1"] != "v1" { t.Errorf("Expected key1=v1 (from file1, preserved), got %v", shared["key1"]) } if shared["key2"] != "override2" { t.Errorf("Expected key2=override2 (from file2, not overridden by file3), got %v", shared["key2"]) } if shared["key3"] != "final3" { t.Errorf("Expected key3=final3 (from file3, final override), got %v", shared["key3"]) } if shared["key4"] != "v4" { t.Errorf("Expected key4=v4 (from file3, new key), got %v", shared["key4"]) } if actual["final"] != "last" { t.Errorf("Expected final=last (new in file3), got %v", actual["final"]) } } // TestHCL_ValuesOverride_DependencyTracking tests that when a value is defined // in multiple files and only the base definition has dependencies, the DAG // still tracks those dependencies correctly. func TestHCL_ValuesOverride_DependencyTracking(t *testing.T) { l := newHCLLoader() files := []string{"testdata/dep.1.hcl", "testdata/dep.2.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error: %s", err.Error()) } // Verify base_var is defined and available if actual["base_var"] != "foundation" { t.Errorf("Expected base_var=foundation, got %v", actual["base_var"]) } // Verify override worked if actual["dependent"] != "override-literal" { t.Errorf("Expected dependent=override-literal (from file2), got %v", actual["dependent"]) } } // TestHCL_ValuesOverride_DependencyInBaseOnly tests the scenario where: // - File 1 defines variable A that depends on variable B (A references B) // - File 2 overrides variable A with a literal (no dependency on B) // - Without tracking all definitions, the DAG might not include B as a dependency // of A, potentially causing evaluation order issues or undefined variable errors func TestHCL_ValuesOverride_DependencyInBaseOnly(t *testing.T) { l := newHCLLoader() files := []string{"testdata/dep_base.hcl", "testdata/dep_override.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error (should not fail due to dependency tracking): %s", err.Error()) } // Verify base variable is defined if actual["image_version"] != "1.0.0" { t.Errorf("Expected image_version=1.0.0, got %v", actual["image_version"]) } // Verify override took effect if actual["container_image"] != "myapp:override" { t.Errorf("Expected container_image=myapp:override, got %v", actual["container_image"]) } } // TestHCL_ValuesOverride_TransitiveDependencies tests that when overriding // a variable that is part of a dependency chain, all transitive dependencies // are properly tracked in the DAG. func TestHCL_ValuesOverride_TransitiveDependencies(t *testing.T) { l := newHCLLoader() files := []string{"testdata/dep_chain_base.hcl", "testdata/dep_chain_override.hcl"} l.AddFiles(files) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error (DAG should track all transitive deps): %s", err.Error()) } // Verify all variables evaluated correctly if actual["version"] != "2.0" { t.Errorf("Expected version=2.0, got %v", actual["version"]) } if actual["image"] != "app:2.0" { t.Errorf("Expected image=app:2.0 (depends on version), got %v", actual["image"]) } if actual["full_path"] != "override/path" { t.Errorf("Expected full_path=override/path (overridden), got %v", actual["full_path"]) } } // TestHCL_CIDRFunctions tests that HCL CIDR functions work correctly func TestHCL_CIDRFunctions(t *testing.T) { l := newHCLLoader() l.AddFiles([]string{"testdata/cidr.hcl"}) actual, err := l.HCLRender() if err != nil { t.Fatalf("Render error: %s", err.Error()) } expected := map[string]any{ "host": "10.0.0.2", "host_neg": "10.255.255.254", "netmask": "255.255.0.0", "subnet": "10.2.0.0/16", "subnets": []any{"10.0.0.0/12", "10.16.0.0/12"}, } if diff := cmp.Diff(expected, actual); diff != "" { t.Error(diff) } }