zcltest: package with mock helpers for testing zcl-based apps
This commit is contained in:
parent
26f1e48014
commit
a414468aac
6
zcltest/doc.go
Normal file
6
zcltest/doc.go
Normal 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
220
zcltest/mock.go
Normal 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
268
zcltest/mock_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user