zcltest: package with mock helpers for testing zcl-based apps

This commit is contained in:
Martin Atkins 2017-07-27 15:59:32 -07:00
parent 26f1e48014
commit a414468aac
3 changed files with 494 additions and 0 deletions

6
zcltest/doc.go Normal file
View File

@ -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

220
zcltest/mock.go Normal file
View File

@ -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
}

268
zcltest/mock_test.go Normal file
View File

@ -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)
}
})
}
}