From ff00ac1b2e9df487fdc3d918aa5655bb36daee05 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:04:01 +0100 Subject: [PATCH] bake: move merge logic to an hcl package and add tests Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 3 +- bake/hclparser/{ => hcl}/LICENSE | 0 bake/hclparser/{ => hcl}/gohcl/decode.go | 0 bake/hclparser/{ => hcl}/gohcl/decode_test.go | 0 bake/hclparser/{ => hcl}/gohcl/doc.go | 0 bake/hclparser/{ => hcl}/gohcl/encode.go | 0 bake/hclparser/{ => hcl}/gohcl/encode_test.go | 0 bake/hclparser/{ => hcl}/gohcl/schema.go | 0 bake/hclparser/{ => hcl}/gohcl/schema_test.go | 0 bake/hclparser/{ => hcl}/gohcl/types.go | 0 bake/hclparser/{ => hcl}/merged.go | 4 +- bake/hclparser/hcl/merged_test.go | 687 ++++++++++++++++++ bake/hclparser/hclparser.go | 2 +- 13 files changed, 691 insertions(+), 5 deletions(-) rename bake/hclparser/{ => hcl}/LICENSE (100%) rename bake/hclparser/{ => hcl}/gohcl/decode.go (100%) rename bake/hclparser/{ => hcl}/gohcl/decode_test.go (100%) rename bake/hclparser/{ => hcl}/gohcl/doc.go (100%) rename bake/hclparser/{ => hcl}/gohcl/encode.go (100%) rename bake/hclparser/{ => hcl}/gohcl/encode_test.go (100%) rename bake/hclparser/{ => hcl}/gohcl/schema.go (100%) rename bake/hclparser/{ => hcl}/gohcl/schema_test.go (100%) rename bake/hclparser/{ => hcl}/gohcl/types.go (100%) rename bake/hclparser/{ => hcl}/merged.go (98%) create mode 100644 bake/hclparser/hcl/merged_test.go diff --git a/bake/bake.go b/bake/bake.go index 8322edbb3ae..6045e6c63b0 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -16,6 +16,7 @@ import ( composecli "github.com/compose-spec/compose-go/v2/cli" "github.com/docker/buildx/bake/hclparser" + hclalt "github.com/docker/buildx/bake/hclparser/hcl" "github.com/docker/buildx/build" controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/buildflags" @@ -343,7 +344,7 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, _ *hclpars var pm hclparser.ParseMeta if len(hclFiles) > 0 { - res, err := hclparser.Parse(hclparser.MergeFiles(hclFiles), hclparser.Opt{ + res, err := hclparser.Parse(hclalt.MergeFiles(hclFiles), hclparser.Opt{ LookupVar: os.LookupEnv, Vars: defaults, ValidateLabel: validateTargetName, diff --git a/bake/hclparser/LICENSE b/bake/hclparser/hcl/LICENSE similarity index 100% rename from bake/hclparser/LICENSE rename to bake/hclparser/hcl/LICENSE diff --git a/bake/hclparser/gohcl/decode.go b/bake/hclparser/hcl/gohcl/decode.go similarity index 100% rename from bake/hclparser/gohcl/decode.go rename to bake/hclparser/hcl/gohcl/decode.go diff --git a/bake/hclparser/gohcl/decode_test.go b/bake/hclparser/hcl/gohcl/decode_test.go similarity index 100% rename from bake/hclparser/gohcl/decode_test.go rename to bake/hclparser/hcl/gohcl/decode_test.go diff --git a/bake/hclparser/gohcl/doc.go b/bake/hclparser/hcl/gohcl/doc.go similarity index 100% rename from bake/hclparser/gohcl/doc.go rename to bake/hclparser/hcl/gohcl/doc.go diff --git a/bake/hclparser/gohcl/encode.go b/bake/hclparser/hcl/gohcl/encode.go similarity index 100% rename from bake/hclparser/gohcl/encode.go rename to bake/hclparser/hcl/gohcl/encode.go diff --git a/bake/hclparser/gohcl/encode_test.go b/bake/hclparser/hcl/gohcl/encode_test.go similarity index 100% rename from bake/hclparser/gohcl/encode_test.go rename to bake/hclparser/hcl/gohcl/encode_test.go diff --git a/bake/hclparser/gohcl/schema.go b/bake/hclparser/hcl/gohcl/schema.go similarity index 100% rename from bake/hclparser/gohcl/schema.go rename to bake/hclparser/hcl/gohcl/schema.go diff --git a/bake/hclparser/gohcl/schema_test.go b/bake/hclparser/hcl/gohcl/schema_test.go similarity index 100% rename from bake/hclparser/gohcl/schema_test.go rename to bake/hclparser/hcl/gohcl/schema_test.go diff --git a/bake/hclparser/gohcl/types.go b/bake/hclparser/hcl/gohcl/types.go similarity index 100% rename from bake/hclparser/gohcl/types.go rename to bake/hclparser/hcl/gohcl/types.go diff --git a/bake/hclparser/merged.go b/bake/hclparser/hcl/merged.go similarity index 98% rename from bake/hclparser/merged.go rename to bake/hclparser/hcl/merged.go index 7fdf1234782..2daec5b74f1 100644 --- a/bake/hclparser/merged.go +++ b/bake/hclparser/hcl/merged.go @@ -1,9 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Forked from https://github.com/hashicorp/hcl/blob/4679383728fe331fc8a6b46036a27b8f818d9bc0/merged.go - -package hclparser +package hcl import ( "fmt" diff --git a/bake/hclparser/hcl/merged_test.go b/bake/hclparser/hcl/merged_test.go new file mode 100644 index 00000000000..19ddc3650e1 --- /dev/null +++ b/bake/hclparser/hcl/merged_test.go @@ -0,0 +1,687 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcl + +import ( + "fmt" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/hcl/v2" +) + +func TestMergedBodiesContent(t *testing.T) { + tests := []struct { + Bodies []hcl.Body + Schema *hcl.BodySchema + Want *hcl.BodyContent + DiagCount int + }{ + { + []hcl.Body{}, + &hcl.BodySchema{}, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + Required: true, + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasAttributes: []string{"name"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"name"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"age"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + { + Name: "age", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + "age": { + Name: "age", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + }, + { + Type: "pizza", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + }, + &testMergedBodiesVictim{ + Name: "second", + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + merged := MergeBodies(test.Bodies) + got, diags := merged.Content(test.Schema) + + if len(diags) != test.DiagCount { + t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf(" - %s", diag) + } + } + + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want)) + } + }) + } +} + +func TestMergeBodiesPartialContent(t *testing.T) { + tests := []struct { + Bodies []hcl.Body + Schema *hcl.BodySchema + WantContent *hcl.BodyContent + WantRemain hcl.Body + DiagCount int + }{ + { + []hcl.Body{}, + &hcl.BodySchema{}, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + mergedBodies{}, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"name", "pizza"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza"}, + }, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza", "soda"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + { + Name: "soda", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + "soda": { + Name: "soda", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza"}, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 1, + "soda": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{}, + HasBlocks: map[string]int{}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{}, + HasBlocks: map[string]int{ + "soda": 2, + }, + }, + }, + 0, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + merged := MergeBodies(test.Bodies) + got, gotRemain, diags := merged.PartialContent(test.Schema) + + if len(diags) != test.DiagCount { + t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf(" - %s", diag) + } + } + + if !reflect.DeepEqual(got, test.WantContent) { + t.Errorf("wrong content result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.WantContent)) + } + + if !reflect.DeepEqual(gotRemain, test.WantRemain) { + t.Errorf("wrong remaining result\ngot: %s\nwant: %s", spew.Sdump(gotRemain), spew.Sdump(test.WantRemain)) + } + }) + } +} + +type testMergedBodiesVictim struct { + Name string + HasAttributes []string + HasBlocks map[string]int + DiagCount int +} + +func (v *testMergedBodiesVictim) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + c, _, d := v.PartialContent(schema) + return c, d +} + +func (v *testMergedBodiesVictim) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + remain := &testMergedBodiesVictim{ + Name: v.Name, + HasAttributes: []string{}, + } + + hasAttrs := map[string]struct{}{} + for _, n := range v.HasAttributes { + hasAttrs[n] = struct{}{} + + var found bool + for _, attrS := range schema.Attributes { + if n == attrS.Name { + found = true + break + } + } + if !found { + remain.HasAttributes = append(remain.HasAttributes, n) + } + } + + content := &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + } + + rng := hcl.Range{ + Filename: v.Name, + } + + for _, attrS := range schema.Attributes { + _, has := hasAttrs[attrS.Name] + if has { + content.Attributes[attrS.Name] = &hcl.Attribute{ + Name: attrS.Name, + NameRange: rng, + } + } + } + + if v.HasBlocks != nil { + for _, blockS := range schema.Blocks { + num := v.HasBlocks[blockS.Type] + for i := 0; i < num; i++ { + content.Blocks = append(content.Blocks, &hcl.Block{ + Type: blockS.Type, + DefRange: rng, + }) + } + } + + remain.HasBlocks = map[string]int{} + for n := range v.HasBlocks { + var found bool + for _, blockS := range schema.Blocks { + if blockS.Type == n { + found = true + break + } + } + if !found { + remain.HasBlocks[n] = v.HasBlocks[n] + } + } + } + + diags := make(hcl.Diagnostics, v.DiagCount) + for i := range diags { + diags[i] = &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Fake diagnostic %d", i), + Detail: "For testing only.", + Context: &rng, + } + } + + return content, remain, diags +} + +func (v *testMergedBodiesVictim) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + attrs := make(map[string]*hcl.Attribute) + + rng := hcl.Range{ + Filename: v.Name, + } + + for _, name := range v.HasAttributes { + attrs[name] = &hcl.Attribute{ + Name: name, + NameRange: rng, + } + } + + diags := make(hcl.Diagnostics, v.DiagCount) + for i := range diags { + diags[i] = &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Fake diagnostic %d", i), + Detail: "For testing only.", + Context: &rng, + } + } + + return attrs, diags +} + +func (v *testMergedBodiesVictim) MissingItemRange() hcl.Range { + return hcl.Range{ + Filename: v.Name, + } +} diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 6933ad71b57..aa460c3949c 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "github.com/docker/buildx/bake/hclparser/gohcl" + "github.com/docker/buildx/bake/hclparser/hcl/gohcl" "github.com/docker/buildx/util/userfunc" "github.com/hashicorp/hcl/v2" "github.com/pkg/errors"