diff --git a/zcltest/doc.go b/zcltest/doc.go new file mode 100644 index 0000000..f0bc927 --- /dev/null +++ b/zcltest/doc.go @@ -0,0 +1,6 @@ +// Package zcltest contains utilities that aim to make it more convenient +// to write tests of code that interacts with the zcl API. +// +// This package is intended for use only in test code. It is optimized for +// convenience of use over all other concerns. +package zcltest diff --git a/zcltest/mock.go b/zcltest/mock.go new file mode 100644 index 0000000..1473f55 --- /dev/null +++ b/zcltest/mock.go @@ -0,0 +1,220 @@ +package zcltest + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-zcl/zcl" +) + +// MockBody returns a zcl.Body implementation that works in terms of a +// caller-constructed zcl.BodyContent, thus avoiding the need to parse +// a "real" zcl config file to use as input to a test. +func MockBody(content *zcl.BodyContent) zcl.Body { + return mockBody{content} +} + +type mockBody struct { + C *zcl.BodyContent +} + +func (b mockBody) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) { + content, remainI, diags := b.PartialContent(schema) + remain := remainI.(mockBody) + for _, attr := range remain.C.Attributes { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Extraneous attribute in mock body", + Detail: fmt.Sprintf("Mock body has extraneous attribute %q.", attr.Name), + Subject: &attr.NameRange, + }) + } + for _, block := range remain.C.Blocks { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Extraneous block in mock body", + Detail: fmt.Sprintf("Mock body has extraneous block of type %q.", block.Type), + Subject: &block.DefRange, + }) + } + return content, diags +} + +func (b mockBody) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) { + ret := &zcl.BodyContent{ + Attributes: map[string]*zcl.Attribute{}, + Blocks: []*zcl.Block{}, + MissingItemRange: b.C.MissingItemRange, + } + remain := &zcl.BodyContent{ + Attributes: map[string]*zcl.Attribute{}, + Blocks: []*zcl.Block{}, + MissingItemRange: b.C.MissingItemRange, + } + var diags zcl.Diagnostics + + if len(schema.Attributes) != 0 { + for _, attrS := range schema.Attributes { + name := attrS.Name + attr, ok := b.C.Attributes[name] + if !ok { + if attrS.Required { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Missing required attribute", + Detail: fmt.Sprintf("Mock body doesn't have attribute %q", name), + Subject: b.C.MissingItemRange.Ptr(), + }) + } + continue + } + ret.Attributes[name] = attr + } + } + + for attrN, attr := range b.C.Attributes { + if _, ok := ret.Attributes[attrN]; !ok { + remain.Attributes[attrN] = attr + } + } + + wantedBlocks := map[string]zcl.BlockHeaderSchema{} + for _, blockS := range schema.Blocks { + wantedBlocks[blockS.Type] = blockS + } + + for _, block := range b.C.Blocks { + if blockS, ok := wantedBlocks[block.Type]; ok { + if len(block.Labels) != len(blockS.LabelNames) { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Wrong number of block labels", + Detail: fmt.Sprintf("Block of type %q requires %d labels, but got %d", blockS.Type, len(blockS.LabelNames), len(block.Labels)), + Subject: b.C.MissingItemRange.Ptr(), + }) + } + + ret.Blocks = append(ret.Blocks, block) + } else { + remain.Blocks = append(remain.Blocks, block) + } + } + + return ret, mockBody{remain}, diags +} + +func (b mockBody) JustAttributes() (zcl.Attributes, zcl.Diagnostics) { + var diags zcl.Diagnostics + if len(b.C.Blocks) != 0 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Mock body has blocks", + Detail: "Can't use JustAttributes on a mock body with blocks.", + Subject: b.C.MissingItemRange.Ptr(), + }) + } + + return b.C.Attributes, diags +} + +func (b mockBody) MissingItemRange() zcl.Range { + return b.C.MissingItemRange +} + +// MockExprLiteral returns a zcl.Expression that evaluates to the given literal +// value. +func MockExprLiteral(val cty.Value) zcl.Expression { + return mockExprLiteral{val} +} + +type mockExprLiteral struct { + V cty.Value +} + +func (e mockExprLiteral) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { + return e.V, nil +} + +func (e mockExprLiteral) Variables() []zcl.Traversal { + return nil +} + +func (e mockExprLiteral) Range() zcl.Range { + return zcl.Range{ + Filename: "MockExprLiteral", + } +} + +func (e mockExprLiteral) StartRange() zcl.Range { + return e.Range() +} + +// MockExprVariable returns a zcl.Expression that evaluates to the value of +// the variable with the given name. +func MockExprVariable(name string) zcl.Expression { + return mockExprVariable(name) +} + +type mockExprVariable string + +func (e mockExprVariable) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { + name := string(e) + for ctx != nil { + if val, ok := ctx.Variables[name]; ok { + return val, nil + } + ctx = ctx.Parent() + } + + // If we fall out here then there is no variable with the given name + return cty.DynamicVal, zcl.Diagnostics{ + { + Severity: zcl.DiagError, + Summary: "Reference to undefined variable", + Detail: fmt.Sprintf("Variable %q is not defined.", name), + }, + } +} + +func (e mockExprVariable) Variables() []zcl.Traversal { + return []zcl.Traversal{ + { + zcl.TraverseRoot{ + Name: string(e), + SrcRange: e.Range(), + }, + }, + } +} + +func (e mockExprVariable) Range() zcl.Range { + return zcl.Range{ + Filename: "MockExprVariable", + } +} + +func (e mockExprVariable) StartRange() zcl.Range { + return e.Range() +} + +// MockAttrs constructs and returns a zcl.Attributes map with attributes +// derived from the given expression map. +// +// Each entry in the map becomes an attribute whose name is the key and +// whose expression is the value. +func MockAttrs(exprs map[string]zcl.Expression) zcl.Attributes { + ret := make(zcl.Attributes) + for name, expr := range exprs { + ret[name] = &zcl.Attribute{ + Name: name, + Expr: expr, + Range: zcl.Range{ + Filename: "MockAttrs", + }, + NameRange: zcl.Range{ + Filename: "MockAttrs", + }, + } + } + return ret +} diff --git a/zcltest/mock_test.go b/zcltest/mock_test.go new file mode 100644 index 0000000..d6c63bc --- /dev/null +++ b/zcltest/mock_test.go @@ -0,0 +1,268 @@ +package zcltest + +import ( + "testing" + + "reflect" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-zcl/zcl" +) + +var mockBodyIsBody zcl.Body = mockBody{} +var mockExprLiteralIsExpr zcl.Expression = mockExprLiteral{} +var mockExprVariableIsExpr zcl.Expression = mockExprVariable("") + +func TestMockBodyPartialContent(t *testing.T) { + tests := map[string]struct { + In *zcl.BodyContent + Schema *zcl.BodySchema + Want *zcl.BodyContent + Remain *zcl.BodyContent + DiagCount int + }{ + "empty": { + &zcl.BodyContent{}, + &zcl.BodySchema{}, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + 0, + }, + "attribute requested": { + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + }), + }, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + }), + Blocks: zcl.Blocks{}, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + 0, + }, + "attribute remains": { + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + }), + }, + &zcl.BodySchema{}, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + }), + Blocks: zcl.Blocks{}, + }, + 0, + }, + "attribute missing": { + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "name", + Required: true, + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + 1, // missing attribute "name" + }, + "block requested, no labels": { + &zcl.BodyContent{ + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "baz", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + 0, + }, + "block requested, wrong labels": { + &zcl.BodyContent{ + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "baz", + LabelNames: []string{"foo"}, + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + 1, // "baz" requires 1 label + }, + "block remains": { + &zcl.BodyContent{ + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + &zcl.BodySchema{}, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{}, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + 0, + }, + "various": { + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + "age": MockExprLiteral(cty.NumberIntVal(32)), + }), + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + { + Type: "bar", + Labels: []string{"foo1"}, + }, + { + Type: "bar", + Labels: []string{"foo2"}, + }, + }, + }, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "name", + }, + }, + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "bar", + LabelNames: []string{"name"}, + }, + }, + }, + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "name": MockExprLiteral(cty.StringVal("Ermintrude")), + }), + Blocks: zcl.Blocks{ + { + Type: "bar", + Labels: []string{"foo1"}, + }, + { + Type: "bar", + Labels: []string{"foo2"}, + }, + }, + }, + &zcl.BodyContent{ + Attributes: MockAttrs(map[string]zcl.Expression{ + "age": MockExprLiteral(cty.NumberIntVal(32)), + }), + Blocks: zcl.Blocks{ + { + Type: "baz", + }, + }, + }, + 0, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + inBody := MockBody(test.In) + got, remainBody, diags := inBody.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.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + + gotRemain := remainBody.(mockBody).C + if !reflect.DeepEqual(gotRemain, test.Remain) { + t.Errorf("wrong remain\ngot: %#v\nwant: %#v", gotRemain, test.Remain) + } + }) + } +}