package hcltest import ( "fmt" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) // MockBody returns a hcl.Body implementation that works in terms of a // caller-constructed hcl.BodyContent, thus avoiding the need to parse // a "real" HCL config file to use as input to a test. func MockBody(content *hcl.BodyContent) hcl.Body { return mockBody{content} } type mockBody struct { C *hcl.BodyContent } func (b mockBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { content, remainI, diags := b.PartialContent(schema) remain := remainI.(mockBody) for _, attr := range remain.C.Attributes { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Extraneous argument in mock body", Detail: fmt.Sprintf("Mock body has extraneous argument %q.", attr.Name), Subject: &attr.NameRange, }) } for _, block := range remain.C.Blocks { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.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 *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { ret := &hcl.BodyContent{ Attributes: map[string]*hcl.Attribute{}, Blocks: []*hcl.Block{}, MissingItemRange: b.C.MissingItemRange, } remain := &hcl.BodyContent{ Attributes: map[string]*hcl.Attribute{}, Blocks: []*hcl.Block{}, MissingItemRange: b.C.MissingItemRange, } var diags hcl.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, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing required argument", Detail: fmt.Sprintf("Mock body doesn't have argument %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]hcl.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, &hcl.Diagnostic{ Severity: hcl.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() (hcl.Attributes, hcl.Diagnostics) { var diags hcl.Diagnostics if len(b.C.Blocks) != 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.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() hcl.Range { return b.C.MissingItemRange } // MockExprLiteral returns a hcl.Expression that evaluates to the given literal // value. func MockExprLiteral(val cty.Value) hcl.Expression { return mockExprLiteral{val} } type mockExprLiteral struct { V cty.Value } func (e mockExprLiteral) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return e.V, nil } func (e mockExprLiteral) Variables() []hcl.Traversal { return nil } func (e mockExprLiteral) Range() hcl.Range { return hcl.Range{ Filename: "MockExprLiteral", } } func (e mockExprLiteral) StartRange() hcl.Range { return e.Range() } // Implementation for hcl.ExprList func (e mockExprLiteral) ExprList() []hcl.Expression { v := e.V ty := v.Type() if v.IsKnown() && !v.IsNull() && (ty.IsListType() || ty.IsTupleType()) { ret := make([]hcl.Expression, 0, v.LengthInt()) for it := v.ElementIterator(); it.Next(); { _, v := it.Element() ret = append(ret, MockExprLiteral(v)) } return ret } return nil } // Implementation for hcl.ExprMap func (e mockExprLiteral) ExprMap() []hcl.KeyValuePair { v := e.V ty := v.Type() if v.IsKnown() && !v.IsNull() && (ty.IsObjectType() || ty.IsMapType()) { ret := make([]hcl.KeyValuePair, 0, v.LengthInt()) for it := v.ElementIterator(); it.Next(); { k, v := it.Element() ret = append(ret, hcl.KeyValuePair{ Key: MockExprLiteral(k), Value: MockExprLiteral(v), }) } return ret } return nil } // MockExprVariable returns a hcl.Expression that evaluates to the value of // the variable with the given name. func MockExprVariable(name string) hcl.Expression { return mockExprVariable(name) } type mockExprVariable string func (e mockExprVariable) Value(ctx *hcl.EvalContext) (cty.Value, hcl.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, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Reference to undefined variable", Detail: fmt.Sprintf("Variable %q is not defined.", name), }, } } func (e mockExprVariable) Variables() []hcl.Traversal { return []hcl.Traversal{ { hcl.TraverseRoot{ Name: string(e), SrcRange: e.Range(), }, }, } } func (e mockExprVariable) Range() hcl.Range { return hcl.Range{ Filename: "MockExprVariable", } } func (e mockExprVariable) StartRange() hcl.Range { return e.Range() } // Implementation for hcl.AbsTraversalForExpr and hcl.RelTraversalForExpr. func (e mockExprVariable) AsTraversal() hcl.Traversal { return hcl.Traversal{ hcl.TraverseRoot{ Name: string(e), SrcRange: e.Range(), }, } } // MockExprTraversal returns a hcl.Expression that evaluates the given // absolute traversal. func MockExprTraversal(traversal hcl.Traversal) hcl.Expression { return mockExprTraversal{ Traversal: traversal, } } // MockExprTraversalSrc is like MockExprTraversal except it takes a // traversal string as defined by the native syntax and parses it first. // // This method is primarily for testing with hard-coded traversal strings, so // it will panic if the given string is not syntactically correct. func MockExprTraversalSrc(src string) hcl.Expression { traversal, diags := hclsyntax.ParseTraversalAbs([]byte(src), "MockExprTraversal", hcl.Pos{}) if diags.HasErrors() { panic("invalid traversal string") } return MockExprTraversal(traversal) } type mockExprTraversal struct { Traversal hcl.Traversal } func (e mockExprTraversal) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return e.Traversal.TraverseAbs(ctx) } func (e mockExprTraversal) Variables() []hcl.Traversal { return []hcl.Traversal{e.Traversal} } func (e mockExprTraversal) Range() hcl.Range { return e.Traversal.SourceRange() } func (e mockExprTraversal) StartRange() hcl.Range { return e.Range() } // Implementation for hcl.AbsTraversalForExpr and hcl.RelTraversalForExpr. func (e mockExprTraversal) AsTraversal() hcl.Traversal { return e.Traversal } func MockExprList(exprs []hcl.Expression) hcl.Expression { return mockExprList{ Exprs: exprs, } } type mockExprList struct { Exprs []hcl.Expression } func (e mockExprList) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { if len(e.Exprs) == 0 { return cty.ListValEmpty(cty.DynamicPseudoType), nil } vals := make([]cty.Value, 0, len(e.Exprs)) var diags hcl.Diagnostics for _, expr := range e.Exprs { val, valDiags := expr.Value(ctx) diags = append(diags, valDiags...) vals = append(vals, val) } return cty.ListVal(vals), diags } func (e mockExprList) Variables() []hcl.Traversal { var traversals []hcl.Traversal for _, expr := range e.Exprs { traversals = append(traversals, expr.Variables()...) } return traversals } func (e mockExprList) Range() hcl.Range { return hcl.Range{ Filename: "MockExprList", } } func (e mockExprList) StartRange() hcl.Range { return e.Range() } // Implementation for hcl.ExprList func (e mockExprList) ExprList() []hcl.Expression { return e.Exprs } // MockAttrs constructs and returns a hcl.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]hcl.Expression) hcl.Attributes { ret := make(hcl.Attributes) for name, expr := range exprs { ret[name] = &hcl.Attribute{ Name: name, Expr: expr, Range: hcl.Range{ Filename: "MockAttrs", }, NameRange: hcl.Range{ Filename: "MockAttrs", }, } } return ret }