github.com/hashicorp/hcl/v2@v2.20.0/cmd/hclspecsuite/test_file.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package main
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  	"github.com/zclconf/go-cty/cty/convert"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/ext/typeexpr"
    14  	"github.com/hashicorp/hcl/v2/gohcl"
    15  )
    16  
    17  type TestFile struct {
    18  	Result     cty.Value
    19  	ResultType cty.Type
    20  
    21  	ChecksTraversals   bool
    22  	ExpectedTraversals []*TestFileExpectTraversal
    23  
    24  	ExpectedDiags []*TestFileExpectDiag
    25  
    26  	ResultRange     hcl.Range
    27  	ResultTypeRange hcl.Range
    28  }
    29  
    30  type TestFileExpectTraversal struct {
    31  	Traversal hcl.Traversal
    32  	Range     hcl.Range
    33  	DeclRange hcl.Range
    34  }
    35  
    36  type TestFileExpectDiag struct {
    37  	Severity  hcl.DiagnosticSeverity
    38  	Range     hcl.Range
    39  	DeclRange hcl.Range
    40  }
    41  
    42  func (r *Runner) LoadTestFile(filename string) (*TestFile, hcl.Diagnostics) {
    43  	f, diags := r.parser.ParseHCLFile(filename)
    44  	if diags.HasErrors() {
    45  		return nil, diags
    46  	}
    47  
    48  	content, moreDiags := f.Body.Content(testFileSchema)
    49  	diags = append(diags, moreDiags...)
    50  	if moreDiags.HasErrors() {
    51  		return nil, diags
    52  	}
    53  
    54  	ret := &TestFile{
    55  		ResultType: cty.DynamicPseudoType,
    56  	}
    57  
    58  	if typeAttr, exists := content.Attributes["result_type"]; exists {
    59  		ty, moreDiags := typeexpr.TypeConstraint(typeAttr.Expr)
    60  		diags = append(diags, moreDiags...)
    61  		if !moreDiags.HasErrors() {
    62  			ret.ResultType = ty
    63  		}
    64  		ret.ResultTypeRange = typeAttr.Expr.Range()
    65  	}
    66  
    67  	if resultAttr, exists := content.Attributes["result"]; exists {
    68  		resultVal, moreDiags := resultAttr.Expr.Value(nil)
    69  		diags = append(diags, moreDiags...)
    70  		if !moreDiags.HasErrors() {
    71  			resultVal, err := convert.Convert(resultVal, ret.ResultType)
    72  			if err != nil {
    73  				diags = diags.Append(&hcl.Diagnostic{
    74  					Severity: hcl.DiagError,
    75  					Summary:  "Invalid result value",
    76  					Detail:   fmt.Sprintf("The result value does not conform to the given result type: %s.", err),
    77  					Subject:  resultAttr.Expr.Range().Ptr(),
    78  				})
    79  			} else {
    80  				ret.Result = resultVal
    81  			}
    82  		}
    83  		ret.ResultRange = resultAttr.Expr.Range()
    84  	}
    85  
    86  	for _, block := range content.Blocks {
    87  		switch block.Type {
    88  
    89  		case "traversals":
    90  			if ret.ChecksTraversals {
    91  				// Indicates a duplicate traversals block
    92  				diags = diags.Append(&hcl.Diagnostic{
    93  					Severity: hcl.DiagError,
    94  					Summary:  "Duplicate \"traversals\" block",
    95  					Detail:   fmt.Sprintf("Only one traversals block is expected."),
    96  					Subject:  &block.TypeRange,
    97  				})
    98  				continue
    99  			}
   100  			expectTraversals, moreDiags := r.decodeTraversalsBlock(block)
   101  			diags = append(diags, moreDiags...)
   102  			if !moreDiags.HasErrors() {
   103  				ret.ChecksTraversals = true
   104  				ret.ExpectedTraversals = expectTraversals
   105  			}
   106  
   107  		case "diagnostics":
   108  			if len(ret.ExpectedDiags) > 0 {
   109  				// Indicates a duplicate diagnostics block
   110  				diags = diags.Append(&hcl.Diagnostic{
   111  					Severity: hcl.DiagError,
   112  					Summary:  "Duplicate \"diagnostics\" block",
   113  					Detail:   fmt.Sprintf("Only one diagnostics block is expected."),
   114  					Subject:  &block.TypeRange,
   115  				})
   116  				continue
   117  			}
   118  			expectDiags, moreDiags := r.decodeDiagnosticsBlock(block)
   119  			diags = append(diags, moreDiags...)
   120  			ret.ExpectedDiags = expectDiags
   121  
   122  		default:
   123  			// Shouldn't get here, because the above cases are exhaustive for
   124  			// our test file schema.
   125  			panic(fmt.Sprintf("unsupported block type %q", block.Type))
   126  		}
   127  	}
   128  
   129  	if ret.Result != cty.NilVal && len(ret.ExpectedDiags) > 0 {
   130  		diags = diags.Append(&hcl.Diagnostic{
   131  			Severity: hcl.DiagError,
   132  			Summary:  "Conflicting spec expectations",
   133  			Detail:   "This test spec includes expected diagnostics, so it may not also include an expected result.",
   134  			Subject:  &content.Attributes["result"].Range,
   135  		})
   136  	}
   137  
   138  	return ret, diags
   139  }
   140  
   141  func (r *Runner) decodeTraversalsBlock(block *hcl.Block) ([]*TestFileExpectTraversal, hcl.Diagnostics) {
   142  	var diags hcl.Diagnostics
   143  
   144  	content, moreDiags := block.Body.Content(testFileTraversalsSchema)
   145  	diags = append(diags, moreDiags...)
   146  	if moreDiags.HasErrors() {
   147  		return nil, diags
   148  	}
   149  
   150  	var ret []*TestFileExpectTraversal
   151  	for _, block := range content.Blocks {
   152  		// There's only one block type in our schema, so we can assume all
   153  		// blocks are of that type.
   154  		expectTraversal, moreDiags := r.decodeTraversalExpectBlock(block)
   155  		diags = append(diags, moreDiags...)
   156  		if expectTraversal != nil {
   157  			ret = append(ret, expectTraversal)
   158  		}
   159  	}
   160  
   161  	return ret, diags
   162  }
   163  
   164  func (r *Runner) decodeTraversalExpectBlock(block *hcl.Block) (*TestFileExpectTraversal, hcl.Diagnostics) {
   165  	var diags hcl.Diagnostics
   166  
   167  	rng, body, moreDiags := r.decodeRangeFromBody(block.Body)
   168  	diags = append(diags, moreDiags...)
   169  
   170  	content, moreDiags := body.Content(testFileTraversalExpectSchema)
   171  	diags = append(diags, moreDiags...)
   172  	if moreDiags.HasErrors() {
   173  		return nil, diags
   174  	}
   175  
   176  	var traversal hcl.Traversal
   177  	{
   178  		refAttr := content.Attributes["ref"]
   179  		traversal, moreDiags = hcl.AbsTraversalForExpr(refAttr.Expr)
   180  		diags = append(diags, moreDiags...)
   181  		if moreDiags.HasErrors() {
   182  			return nil, diags
   183  		}
   184  	}
   185  
   186  	return &TestFileExpectTraversal{
   187  		Traversal: traversal,
   188  		Range:     rng,
   189  		DeclRange: block.DefRange,
   190  	}, diags
   191  }
   192  
   193  func (r *Runner) decodeDiagnosticsBlock(block *hcl.Block) ([]*TestFileExpectDiag, hcl.Diagnostics) {
   194  	var diags hcl.Diagnostics
   195  
   196  	content, moreDiags := block.Body.Content(testFileDiagnosticsSchema)
   197  	diags = append(diags, moreDiags...)
   198  	if moreDiags.HasErrors() {
   199  		return nil, diags
   200  	}
   201  
   202  	if len(content.Blocks) == 0 {
   203  		diags = diags.Append(&hcl.Diagnostic{
   204  			Severity: hcl.DiagError,
   205  			Summary:  "Empty diagnostics block",
   206  			Detail:   "If a diagnostics block is present, at least one expectation statement (\"error\" or \"warning\" block) must be included.",
   207  			Subject:  &block.TypeRange,
   208  		})
   209  		return nil, diags
   210  	}
   211  
   212  	ret := make([]*TestFileExpectDiag, 0, len(content.Blocks))
   213  	for _, block := range content.Blocks {
   214  		rng, remain, moreDiags := r.decodeRangeFromBody(block.Body)
   215  		diags = append(diags, moreDiags...)
   216  		if diags.HasErrors() {
   217  			continue
   218  		}
   219  
   220  		// Should have nothing else in the block aside from the range definition.
   221  		_, moreDiags = remain.Content(&hcl.BodySchema{})
   222  		diags = append(diags, moreDiags...)
   223  
   224  		var severity hcl.DiagnosticSeverity
   225  		switch block.Type {
   226  		case "error":
   227  			severity = hcl.DiagError
   228  		case "warning":
   229  			severity = hcl.DiagWarning
   230  		default:
   231  			panic(fmt.Sprintf("unsupported block type %q", block.Type))
   232  		}
   233  
   234  		ret = append(ret, &TestFileExpectDiag{
   235  			Severity:  severity,
   236  			Range:     rng,
   237  			DeclRange: block.TypeRange,
   238  		})
   239  	}
   240  	return ret, diags
   241  }
   242  
   243  func (r *Runner) decodeRangeFromBody(body hcl.Body) (hcl.Range, hcl.Body, hcl.Diagnostics) {
   244  	type RawPos struct {
   245  		Line   int `hcl:"line"`
   246  		Column int `hcl:"column"`
   247  		Byte   int `hcl:"byte"`
   248  	}
   249  	type RawRange struct {
   250  		From   RawPos   `hcl:"from,block"`
   251  		To     RawPos   `hcl:"to,block"`
   252  		Remain hcl.Body `hcl:",remain"`
   253  	}
   254  
   255  	var raw RawRange
   256  	diags := gohcl.DecodeBody(body, nil, &raw)
   257  
   258  	return hcl.Range{
   259  		// We intentionally omit Filename here, because the test spec doesn't
   260  		// need to specify that explicitly: we can infer it to be the file
   261  		// path we pass to hcldec.
   262  		Start: hcl.Pos{
   263  			Line:   raw.From.Line,
   264  			Column: raw.From.Column,
   265  			Byte:   raw.From.Byte,
   266  		},
   267  		End: hcl.Pos{
   268  			Line:   raw.To.Line,
   269  			Column: raw.To.Column,
   270  			Byte:   raw.To.Byte,
   271  		},
   272  	}, raw.Remain, diags
   273  }
   274  
   275  var testFileSchema = &hcl.BodySchema{
   276  	Attributes: []hcl.AttributeSchema{
   277  		{
   278  			Name: "result",
   279  		},
   280  		{
   281  			Name: "result_type",
   282  		},
   283  	},
   284  	Blocks: []hcl.BlockHeaderSchema{
   285  		{
   286  			Type: "traversals",
   287  		},
   288  		{
   289  			Type: "diagnostics",
   290  		},
   291  	},
   292  }
   293  
   294  var testFileTraversalsSchema = &hcl.BodySchema{
   295  	Blocks: []hcl.BlockHeaderSchema{
   296  		{
   297  			Type: "expect",
   298  		},
   299  	},
   300  }
   301  
   302  var testFileTraversalExpectSchema = &hcl.BodySchema{
   303  	Attributes: []hcl.AttributeSchema{
   304  		{
   305  			Name:     "ref",
   306  			Required: true,
   307  		},
   308  	},
   309  	Blocks: []hcl.BlockHeaderSchema{
   310  		{
   311  			Type: "range",
   312  		},
   313  	},
   314  }
   315  
   316  var testFileDiagnosticsSchema = &hcl.BodySchema{
   317  	Blocks: []hcl.BlockHeaderSchema{
   318  		{
   319  			Type: "error",
   320  		},
   321  		{
   322  			Type: "warning",
   323  		},
   324  	},
   325  }
   326  
   327  var testFileRangeSchema = &hcl.BodySchema{
   328  	Blocks: []hcl.BlockHeaderSchema{
   329  		{
   330  			Type: "from",
   331  		},
   332  		{
   333  			Type: "to",
   334  		},
   335  	},
   336  }
   337  
   338  var testFilePosSchema = &hcl.BodySchema{
   339  	Attributes: []hcl.AttributeSchema{
   340  		{
   341  			Name:     "line",
   342  			Required: true,
   343  		},
   344  		{
   345  			Name:     "column",
   346  			Required: true,
   347  		},
   348  		{
   349  			Name:     "byte",
   350  			Required: true,
   351  		},
   352  	},
   353  }