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 }