Beginnings of automated test suite for spec compliance.
The test cases are far from complete, but the mechanism is in place.
This commit is contained in:
commit
c79a9bd509
@ -28,6 +28,7 @@ var (
|
||||
outputFile = flag.StringP("out", "o", "", "write to the given file, instead of stdout")
|
||||
diagsFormat = flag.StringP("diags", "", "", "format any returned diagnostics in the given format; currently only \"json\" is accepted")
|
||||
showVarRefs = flag.BoolP("var-refs", "", false, "rather than decoding input, produce a JSON description of the variables referenced by it")
|
||||
withType = flag.BoolP("with-type", "", false, "include an additional object level at the top describing the HCL-oriented type of the result value")
|
||||
showVersion = flag.BoolP("version", "v", false, "show the version number and immediately exit")
|
||||
)
|
||||
|
||||
@ -146,7 +147,13 @@ func realmain(args []string) error {
|
||||
}
|
||||
} else {
|
||||
for _, filename := range args {
|
||||
f, fDiags := parser.ParseHCLFile(filename)
|
||||
var f *hcl.File
|
||||
var fDiags hcl.Diagnostics
|
||||
if strings.HasSuffix(filename, ".json") {
|
||||
f, fDiags = parser.ParseJSONFile(filename)
|
||||
} else {
|
||||
f, fDiags = parser.ParseHCLFile(filename)
|
||||
}
|
||||
diags = append(diags, fDiags...)
|
||||
if !fDiags.HasErrors() {
|
||||
bodies = append(bodies, f.Body)
|
||||
@ -185,7 +192,13 @@ func realmain(args []string) error {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
out, err := ctyjson.Marshal(val, val.Type())
|
||||
wantType := val.Type()
|
||||
if *withType {
|
||||
// We'll instead ask to encode as dynamic, which will make the
|
||||
// marshaler include type information.
|
||||
wantType = cty.DynamicPseudoType
|
||||
}
|
||||
out, err := ctyjson.Marshal(val, wantType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
4
cmd/hclspecsuite/README.md
Normal file
4
cmd/hclspecsuite/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# `hclspecsuite`
|
||||
|
||||
`hclspecsuite` is the test harness for
|
||||
[the HCL specification test suite](../../specsuite/README.md).
|
108
cmd/hclspecsuite/diagnostics.go
Normal file
108
cmd/hclspecsuite/diagnostics.go
Normal file
@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func decodeJSONDiagnostics(src []byte) hcl.Diagnostics {
|
||||
type PosJSON struct {
|
||||
Line int `json:"line"`
|
||||
Column int `json:"column"`
|
||||
Byte int `json:"byte"`
|
||||
}
|
||||
type RangeJSON struct {
|
||||
Filename string `json:"filename"`
|
||||
Start PosJSON `json:"start"`
|
||||
End PosJSON `json:"end"`
|
||||
}
|
||||
type DiagnosticJSON struct {
|
||||
Severity string `json:"severity"`
|
||||
Summary string `json:"summary"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Subject *RangeJSON `json:"subject,omitempty"`
|
||||
}
|
||||
type DiagnosticsJSON struct {
|
||||
Diagnostics []DiagnosticJSON `json:"diagnostics"`
|
||||
}
|
||||
|
||||
var raw DiagnosticsJSON
|
||||
var diags hcl.Diagnostics
|
||||
err := json.Unmarshal(src, &raw)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec diagnostics result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec produced invalid diagnostics: %s.", err),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
if len(raw.Diagnostics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
diags = make(hcl.Diagnostics, 0, len(raw.Diagnostics))
|
||||
for _, rawDiag := range raw.Diagnostics {
|
||||
var severity hcl.DiagnosticSeverity
|
||||
switch rawDiag.Severity {
|
||||
case "error":
|
||||
severity = hcl.DiagError
|
||||
case "warning":
|
||||
severity = hcl.DiagWarning
|
||||
default:
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec diagnostics result",
|
||||
Detail: fmt.Sprintf("Diagnostic has unsupported severity %q.", rawDiag.Severity),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
diag := &hcl.Diagnostic{
|
||||
Severity: severity,
|
||||
Summary: rawDiag.Summary,
|
||||
Detail: rawDiag.Detail,
|
||||
}
|
||||
if rawDiag.Subject != nil {
|
||||
rawRange := rawDiag.Subject
|
||||
diag.Subject = &hcl.Range{
|
||||
Filename: rawRange.Filename,
|
||||
Start: hcl.Pos{
|
||||
Line: rawRange.Start.Line,
|
||||
Column: rawRange.Start.Column,
|
||||
Byte: rawRange.Start.Byte,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: rawRange.End.Line,
|
||||
Column: rawRange.End.Column,
|
||||
Byte: rawRange.End.Byte,
|
||||
},
|
||||
}
|
||||
}
|
||||
diags = append(diags, diag)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func severityString(severity hcl.DiagnosticSeverity) string {
|
||||
switch severity {
|
||||
case hcl.DiagError:
|
||||
return "error"
|
||||
case hcl.DiagWarning:
|
||||
return "warning"
|
||||
default:
|
||||
return "unsupported-severity"
|
||||
}
|
||||
}
|
||||
|
||||
func rangeString(rng hcl.Range) string {
|
||||
return fmt.Sprintf(
|
||||
"from line %d column %d byte %d to line %d column %d byte %d",
|
||||
rng.Start.Line, rng.Start.Column, rng.Start.Byte,
|
||||
rng.End.Line, rng.End.Column, rng.End.Byte,
|
||||
)
|
||||
}
|
8
cmd/hclspecsuite/log.go
Normal file
8
cmd/hclspecsuite/log.go
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
type LogBeginCallback func(testName string, testFile *TestFile)
|
||||
type LogProblemsCallback func(testName string, testFile *TestFile, diags hcl.Diagnostics)
|
71
cmd/hclspecsuite/main.go
Normal file
71
cmd/hclspecsuite/main.go
Normal file
@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hclparse"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(realMain(os.Args[1:]))
|
||||
}
|
||||
|
||||
func realMain(args []string) int {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: hclspecsuite <tests-dir> <hcldec-file>\n")
|
||||
return 2
|
||||
}
|
||||
|
||||
testsDir := args[0]
|
||||
hcldecPath := args[1]
|
||||
|
||||
hcldecPath, err := exec.LookPath(hcldecPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
parser := hclparse.NewParser()
|
||||
|
||||
color := terminal.IsTerminal(int(os.Stderr.Fd()))
|
||||
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
w = 80
|
||||
}
|
||||
diagWr := hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color)
|
||||
var diagCount int
|
||||
|
||||
runner := &Runner{
|
||||
parser: parser,
|
||||
hcldecPath: hcldecPath,
|
||||
baseDir: testsDir,
|
||||
logBegin: func(name string, file *TestFile) {
|
||||
fmt.Printf("- %s\n", name)
|
||||
},
|
||||
logProblems: func(name string, file *TestFile, diags hcl.Diagnostics) {
|
||||
if len(diags) != 0 {
|
||||
os.Stderr.WriteString("\n")
|
||||
diagWr.WriteDiagnostics(diags)
|
||||
diagCount += len(diags)
|
||||
}
|
||||
fmt.Printf("- %s\n", name)
|
||||
},
|
||||
}
|
||||
diags := runner.Run()
|
||||
|
||||
if len(diags) != 0 {
|
||||
os.Stderr.WriteString("\n\n\n== Test harness problems:\n\n")
|
||||
diagWr.WriteDiagnostics(diags)
|
||||
diagCount += len(diags)
|
||||
}
|
||||
|
||||
if diagCount > 0 {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
521
cmd/hclspecsuite/runner.go
Normal file
521
cmd/hclspecsuite/runner.go
Normal file
@ -0,0 +1,521 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/hashicorp/hcl2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hclparse"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
parser *hclparse.Parser
|
||||
hcldecPath string
|
||||
baseDir string
|
||||
logBegin LogBeginCallback
|
||||
logProblems LogProblemsCallback
|
||||
}
|
||||
|
||||
func (r *Runner) Run() hcl.Diagnostics {
|
||||
return r.runDir(r.baseDir)
|
||||
}
|
||||
|
||||
func (r *Runner) runDir(dir string) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
infos, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to read test directory",
|
||||
Detail: fmt.Sprintf("The directory %q could not be opened: %s.", dir, err),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
var tests []string
|
||||
var subDirs []string
|
||||
for _, info := range infos {
|
||||
name := info.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
subDirs = append(subDirs, name)
|
||||
}
|
||||
if strings.HasSuffix(name, ".t") {
|
||||
tests = append(tests, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(tests)
|
||||
sort.Strings(subDirs)
|
||||
|
||||
for _, filename := range tests {
|
||||
filename = filepath.Join(dir, filename)
|
||||
testDiags := r.runTest(filename)
|
||||
diags = append(diags, testDiags...)
|
||||
}
|
||||
|
||||
for _, dirName := range subDirs {
|
||||
dir := filepath.Join(dir, dirName)
|
||||
dirDiags := r.runDir(dir)
|
||||
diags = append(diags, dirDiags...)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (r *Runner) runTest(filename string) hcl.Diagnostics {
|
||||
prettyName := r.prettyTestName(filename)
|
||||
tf, diags := r.LoadTestFile(filename)
|
||||
if diags.HasErrors() {
|
||||
// We'll still log, so it's clearer which test the diagnostics belong to.
|
||||
if r.logBegin != nil {
|
||||
r.logBegin(prettyName, nil)
|
||||
}
|
||||
if r.logProblems != nil {
|
||||
r.logProblems(prettyName, nil, diags)
|
||||
return nil // don't duplicate the diagnostics we already reported
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
if r.logBegin != nil {
|
||||
r.logBegin(prettyName, tf)
|
||||
}
|
||||
|
||||
basePath := filename[:len(filename)-2]
|
||||
specFilename := basePath + ".hcldec"
|
||||
nativeFilename := basePath + ".hcl"
|
||||
jsonFilename := basePath + ".hcl.json"
|
||||
|
||||
// We'll add the source code of the spec file to our own parser, even
|
||||
// though it'll actually be parsed by the hcldec child process, since that
|
||||
// way we can produce nice diagnostic messages if hcldec fails to process
|
||||
// the spec file.
|
||||
src, err := ioutil.ReadFile(specFilename)
|
||||
if err == nil {
|
||||
r.parser.AddFile(specFilename, &hcl.File{
|
||||
Bytes: src,
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := os.Stat(specFilename); err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing .hcldec file",
|
||||
Detail: fmt.Sprintf("No specification file for test %s: %s.", prettyName, err),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
if _, err := os.Stat(nativeFilename); err == nil {
|
||||
moreDiags := r.runTestInput(specFilename, nativeFilename, tf)
|
||||
diags = append(diags, moreDiags...)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(jsonFilename); err == nil {
|
||||
moreDiags := r.runTestInput(specFilename, jsonFilename, tf)
|
||||
diags = append(diags, moreDiags...)
|
||||
}
|
||||
|
||||
if r.logProblems != nil {
|
||||
r.logProblems(prettyName, nil, diags)
|
||||
return nil // don't duplicate the diagnostics we already reported
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (r *Runner) runTestInput(specFilename, inputFilename string, tf *TestFile) hcl.Diagnostics {
|
||||
// We'll add the source code of the input file to our own parser, even
|
||||
// though it'll actually be parsed by the hcldec child process, since that
|
||||
// way we can produce nice diagnostic messages if hcldec fails to process
|
||||
// the input file.
|
||||
src, err := ioutil.ReadFile(inputFilename)
|
||||
if err == nil {
|
||||
r.parser.AddFile(inputFilename, &hcl.File{
|
||||
Bytes: src,
|
||||
})
|
||||
}
|
||||
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
if tf.ChecksTraversals {
|
||||
gotTraversals, moreDiags := r.hcldecVariables(specFilename, inputFilename)
|
||||
diags = append(diags, moreDiags...)
|
||||
if !moreDiags.HasErrors() {
|
||||
expected := tf.ExpectedTraversals
|
||||
for _, got := range gotTraversals {
|
||||
e := findTraversalSpec(got, expected)
|
||||
rng := got.SourceRange()
|
||||
if e == nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unexpected traversal",
|
||||
Detail: "Detected traversal that is not indicated as expected in the test file.",
|
||||
Subject: &rng,
|
||||
})
|
||||
} else {
|
||||
moreDiags := checkTraversalsMatch(got, inputFilename, e)
|
||||
diags = append(diags, moreDiags...)
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any traversals that didn't show up at all.
|
||||
for _, e := range expected {
|
||||
if t := findTraversalForSpec(e, gotTraversals); t == nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing expected traversal",
|
||||
Detail: "This expected traversal was not detected.",
|
||||
Subject: e.Traversal.SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val, transformDiags := r.hcldecTransform(specFilename, inputFilename)
|
||||
if len(tf.ExpectedDiags) == 0 {
|
||||
diags = append(diags, transformDiags...)
|
||||
if transformDiags.HasErrors() {
|
||||
// If hcldec failed then there's no point in continuing.
|
||||
return diags
|
||||
}
|
||||
|
||||
if errs := val.Type().TestConformance(tf.ResultType); len(errs) > 0 {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect result type",
|
||||
Detail: fmt.Sprintf(
|
||||
"Input file %s produced %s, but was expecting %s.",
|
||||
inputFilename, typeexpr.TypeString(val.Type()), typeexpr.TypeString(tf.ResultType),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if tf.Result != cty.NilVal {
|
||||
cmpVal, err := convert.Convert(tf.Result, tf.ResultType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect type for result value",
|
||||
Detail: fmt.Sprintf(
|
||||
"Result does not conform to the given result type: %s.", err,
|
||||
),
|
||||
Subject: &tf.ResultRange,
|
||||
})
|
||||
} else {
|
||||
if !val.RawEquals(cmpVal) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect result value",
|
||||
Detail: fmt.Sprintf(
|
||||
"Input file %s produced %#v, but was expecting %#v.",
|
||||
inputFilename, val, tf.Result,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We're expecting diagnostics, and so we'll need to correlate the
|
||||
// severities and source ranges of our actual diagnostics against
|
||||
// what we were expecting.
|
||||
type DiagnosticEntry struct {
|
||||
Severity hcl.DiagnosticSeverity
|
||||
Range hcl.Range
|
||||
}
|
||||
got := make(map[DiagnosticEntry]*hcl.Diagnostic)
|
||||
want := make(map[DiagnosticEntry]hcl.Range)
|
||||
for _, diag := range transformDiags {
|
||||
if diag.Subject == nil {
|
||||
// Sourceless diagnostics can never be expected, so we'll just
|
||||
// pass these through as-is and assume they are hcldec
|
||||
// operational errors.
|
||||
diags = append(diags, diag)
|
||||
continue
|
||||
}
|
||||
if diag.Subject.Filename != inputFilename {
|
||||
// If the problem is for something other than the input file
|
||||
// then it can't be expected.
|
||||
diags = append(diags, diag)
|
||||
continue
|
||||
}
|
||||
entry := DiagnosticEntry{
|
||||
Severity: diag.Severity,
|
||||
Range: *diag.Subject,
|
||||
}
|
||||
got[entry] = diag
|
||||
}
|
||||
for _, e := range tf.ExpectedDiags {
|
||||
e.Range.Filename = inputFilename // assumed here, since we don't allow any other filename to be expected
|
||||
entry := DiagnosticEntry{
|
||||
Severity: e.Severity,
|
||||
Range: e.Range,
|
||||
}
|
||||
want[entry] = e.DeclRange
|
||||
}
|
||||
|
||||
for gotEntry, diag := range got {
|
||||
if _, wanted := want[gotEntry]; !wanted {
|
||||
// Pass through the diagnostic itself so the user can see what happened
|
||||
diags = append(diags, diag)
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unexpected diagnostic",
|
||||
Detail: fmt.Sprintf(
|
||||
"No %s diagnostic was expected %s. The unexpected diagnostic was shown above.",
|
||||
severityString(gotEntry.Severity), rangeString(gotEntry.Range),
|
||||
),
|
||||
Subject: &gotEntry.Range,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for wantEntry, declRange := range want {
|
||||
if _, gotted := got[wantEntry]; !gotted {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing expected diagnostic",
|
||||
Detail: fmt.Sprintf(
|
||||
"No %s diagnostic was generated %s.",
|
||||
severityString(wantEntry.Severity), rangeString(wantEntry.Range),
|
||||
),
|
||||
Subject: &declRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (r *Runner) hcldecTransform(specFile, inputFile string) (cty.Value, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
var outBuffer bytes.Buffer
|
||||
var errBuffer bytes.Buffer
|
||||
|
||||
cmd := &exec.Cmd{
|
||||
Path: r.hcldecPath,
|
||||
Args: []string{
|
||||
r.hcldecPath,
|
||||
"--spec=" + specFile,
|
||||
"--diags=json",
|
||||
"--with-type",
|
||||
inputFile,
|
||||
},
|
||||
Stdout: &outBuffer,
|
||||
Stderr: &errBuffer,
|
||||
}
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if _, isExit := err.(*exec.ExitError); !isExit {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to run hcldec",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec failed to start: %s.", err),
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
// If we exited unsuccessfully then we'll expect diagnostics on stderr
|
||||
moreDiags := decodeJSONDiagnostics(errBuffer.Bytes())
|
||||
diags = append(diags, moreDiags...)
|
||||
return cty.DynamicVal, diags
|
||||
} else {
|
||||
// Otherwise, we expect a JSON result value on stdout. Since we used
|
||||
// --with-type above, we can decode as DynamicPseudoType to recover
|
||||
// exactly the type that was saved, without the usual JSON lossiness.
|
||||
val, err := ctyjson.Unmarshal(outBuffer.Bytes(), cty.DynamicPseudoType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec produced an invalid result: %s.", err),
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
return val, diags
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) hcldecVariables(specFile, inputFile string) ([]hcl.Traversal, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
var outBuffer bytes.Buffer
|
||||
var errBuffer bytes.Buffer
|
||||
|
||||
cmd := &exec.Cmd{
|
||||
Path: r.hcldecPath,
|
||||
Args: []string{
|
||||
r.hcldecPath,
|
||||
"--spec=" + specFile,
|
||||
"--diags=json",
|
||||
"--var-refs",
|
||||
inputFile,
|
||||
},
|
||||
Stdout: &outBuffer,
|
||||
Stderr: &errBuffer,
|
||||
}
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if _, isExit := err.(*exec.ExitError); !isExit {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to run hcldec",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec (evaluating input) failed to start: %s.", err),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// If we exited unsuccessfully then we'll expect diagnostics on stderr
|
||||
moreDiags := decodeJSONDiagnostics(errBuffer.Bytes())
|
||||
diags = append(diags, moreDiags...)
|
||||
return nil, diags
|
||||
} else {
|
||||
// Otherwise, we expect a JSON description of the traversals on stdout.
|
||||
type PosJSON struct {
|
||||
Line int `json:"line"`
|
||||
Column int `json:"column"`
|
||||
Byte int `json:"byte"`
|
||||
}
|
||||
type RangeJSON struct {
|
||||
Filename string `json:"filename"`
|
||||
Start PosJSON `json:"start"`
|
||||
End PosJSON `json:"end"`
|
||||
}
|
||||
type StepJSON struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key json.RawMessage `json:"key,omitempty"`
|
||||
Range RangeJSON `json:"range"`
|
||||
}
|
||||
type TraversalJSON struct {
|
||||
Steps []StepJSON `json:"steps"`
|
||||
}
|
||||
|
||||
var raw []TraversalJSON
|
||||
err := json.Unmarshal(outBuffer.Bytes(), &raw)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec (with --var-refs) produced an invalid result: %s.", err),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
var ret []hcl.Traversal
|
||||
if len(raw) == 0 {
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
ret = make([]hcl.Traversal, 0, len(raw))
|
||||
for _, rawT := range raw {
|
||||
traversal := make(hcl.Traversal, 0, len(rawT.Steps))
|
||||
for _, rawS := range rawT.Steps {
|
||||
rng := hcl.Range{
|
||||
Filename: rawS.Range.Filename,
|
||||
Start: hcl.Pos{
|
||||
Line: rawS.Range.Start.Line,
|
||||
Column: rawS.Range.Start.Column,
|
||||
Byte: rawS.Range.Start.Byte,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: rawS.Range.End.Line,
|
||||
Column: rawS.Range.End.Column,
|
||||
Byte: rawS.Range.End.Byte,
|
||||
},
|
||||
}
|
||||
|
||||
switch rawS.Kind {
|
||||
|
||||
case "root":
|
||||
traversal = append(traversal, hcl.TraverseRoot{
|
||||
Name: rawS.Name,
|
||||
SrcRange: rng,
|
||||
})
|
||||
|
||||
case "attr":
|
||||
traversal = append(traversal, hcl.TraverseAttr{
|
||||
Name: rawS.Name,
|
||||
SrcRange: rng,
|
||||
})
|
||||
|
||||
case "index":
|
||||
ty, err := ctyjson.ImpliedType([]byte(rawS.Key))
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec (with --var-refs) produced an invalid result: traversal step has invalid index key %s.", rawS.Key),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
keyVal, err := ctyjson.Unmarshal([]byte(rawS.Key), ty)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec (with --var-refs) produced a result with an invalid index key %s: %s.", rawS.Key, err),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
traversal = append(traversal, hcl.TraverseIndex{
|
||||
Key: keyVal,
|
||||
SrcRange: rng,
|
||||
})
|
||||
|
||||
default:
|
||||
// Should never happen since the above cases are exhaustive,
|
||||
// but we'll catch it gracefully since this is coming from
|
||||
// a possibly-buggy hcldec implementation that we're testing.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to parse hcldec result",
|
||||
Detail: fmt.Sprintf("Sub-program hcldec (with --var-refs) produced an invalid result: traversal step of unsupported kind %q.", rawS.Kind),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
|
||||
ret = append(ret, traversal)
|
||||
}
|
||||
return ret, diags
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) prettyDirName(dir string) string {
|
||||
rel, err := filepath.Rel(r.baseDir, dir)
|
||||
if err != nil {
|
||||
return filepath.ToSlash(dir)
|
||||
}
|
||||
return filepath.ToSlash(rel)
|
||||
}
|
||||
|
||||
func (r *Runner) prettyTestName(filename string) string {
|
||||
dir := filepath.Dir(filename)
|
||||
dirName := r.prettyDirName(dir)
|
||||
filename = filepath.Base(filename)
|
||||
testName := filename[:len(filename)-2]
|
||||
if dirName == "." {
|
||||
return testName
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", dirName, testName)
|
||||
}
|
350
cmd/hclspecsuite/test_file.go
Normal file
350
cmd/hclspecsuite/test_file.go
Normal file
@ -0,0 +1,350 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
"github.com/hashicorp/hcl2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
type TestFile struct {
|
||||
Result cty.Value
|
||||
ResultType cty.Type
|
||||
|
||||
ChecksTraversals bool
|
||||
ExpectedTraversals []*TestFileExpectTraversal
|
||||
|
||||
ExpectedDiags []*TestFileExpectDiag
|
||||
|
||||
ResultRange hcl.Range
|
||||
ResultTypeRange hcl.Range
|
||||
}
|
||||
|
||||
type TestFileExpectTraversal struct {
|
||||
Traversal hcl.Traversal
|
||||
Range hcl.Range
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
type TestFileExpectDiag struct {
|
||||
Severity hcl.DiagnosticSeverity
|
||||
Range hcl.Range
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func (r *Runner) LoadTestFile(filename string) (*TestFile, hcl.Diagnostics) {
|
||||
f, diags := r.parser.ParseHCLFile(filename)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
content, moreDiags := f.Body.Content(testFileSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ret := &TestFile{
|
||||
ResultType: cty.DynamicPseudoType,
|
||||
}
|
||||
|
||||
if typeAttr, exists := content.Attributes["result_type"]; exists {
|
||||
ty, moreDiags := typeexpr.TypeConstraint(typeAttr.Expr)
|
||||
diags = append(diags, moreDiags...)
|
||||
if !moreDiags.HasErrors() {
|
||||
ret.ResultType = ty
|
||||
}
|
||||
ret.ResultTypeRange = typeAttr.Expr.Range()
|
||||
}
|
||||
|
||||
if resultAttr, exists := content.Attributes["result"]; exists {
|
||||
resultVal, moreDiags := resultAttr.Expr.Value(nil)
|
||||
diags = append(diags, moreDiags...)
|
||||
if !moreDiags.HasErrors() {
|
||||
resultVal, err := convert.Convert(resultVal, ret.ResultType)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid result value",
|
||||
Detail: fmt.Sprintf("The result value does not conform to the given result type: %s.", err),
|
||||
Subject: resultAttr.Expr.Range().Ptr(),
|
||||
})
|
||||
} else {
|
||||
ret.Result = resultVal
|
||||
}
|
||||
}
|
||||
ret.ResultRange = resultAttr.Expr.Range()
|
||||
}
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
|
||||
case "traversals":
|
||||
if ret.ChecksTraversals {
|
||||
// Indicates a duplicate traversals block
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate \"traversals\" block",
|
||||
Detail: fmt.Sprintf("Only one traversals block is expected."),
|
||||
Subject: &block.TypeRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
expectTraversals, moreDiags := r.decodeTraversalsBlock(block)
|
||||
diags = append(diags, moreDiags...)
|
||||
if !moreDiags.HasErrors() {
|
||||
ret.ChecksTraversals = true
|
||||
ret.ExpectedTraversals = expectTraversals
|
||||
}
|
||||
|
||||
case "diagnostics":
|
||||
if len(ret.ExpectedDiags) > 0 {
|
||||
// Indicates a duplicate diagnostics block
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate \"diagnostics\" block",
|
||||
Detail: fmt.Sprintf("Only one diagnostics block is expected."),
|
||||
Subject: &block.TypeRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
expectDiags, moreDiags := r.decodeDiagnosticsBlock(block)
|
||||
diags = append(diags, moreDiags...)
|
||||
ret.ExpectedDiags = expectDiags
|
||||
|
||||
default:
|
||||
// Shouldn't get here, because the above cases are exhaustive for
|
||||
// our test file schema.
|
||||
panic(fmt.Sprintf("unsupported block type %q", block.Type))
|
||||
}
|
||||
}
|
||||
|
||||
if ret.Result != cty.NilVal && len(ret.ExpectedDiags) > 0 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Conflicting spec expectations",
|
||||
Detail: "This test spec includes expected diagnostics, so it may not also include an expected result.",
|
||||
Subject: &content.Attributes["result"].Range,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func (r *Runner) decodeTraversalsBlock(block *hcl.Block) ([]*TestFileExpectTraversal, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
content, moreDiags := block.Body.Content(testFileTraversalsSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
var ret []*TestFileExpectTraversal
|
||||
for _, block := range content.Blocks {
|
||||
// There's only one block type in our schema, so we can assume all
|
||||
// blocks are of that type.
|
||||
expectTraversal, moreDiags := r.decodeTraversalExpectBlock(block)
|
||||
diags = append(diags, moreDiags...)
|
||||
if expectTraversal != nil {
|
||||
ret = append(ret, expectTraversal)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func (r *Runner) decodeTraversalExpectBlock(block *hcl.Block) (*TestFileExpectTraversal, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
rng, body, moreDiags := r.decodeRangeFromBody(block.Body)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
content, moreDiags := body.Content(testFileTraversalExpectSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
var traversal hcl.Traversal
|
||||
{
|
||||
refAttr := content.Attributes["ref"]
|
||||
traversal, moreDiags = hcl.AbsTraversalForExpr(refAttr.Expr)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
|
||||
return &TestFileExpectTraversal{
|
||||
Traversal: traversal,
|
||||
Range: rng,
|
||||
DeclRange: block.DefRange,
|
||||
}, diags
|
||||
}
|
||||
|
||||
func (r *Runner) decodeDiagnosticsBlock(block *hcl.Block) ([]*TestFileExpectDiag, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
content, moreDiags := block.Body.Content(testFileDiagnosticsSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if len(content.Blocks) == 0 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Empty diagnostics block",
|
||||
Detail: "If a diagnostics block is present, at least one expectation statement (\"error\" or \"warning\" block) must be included.",
|
||||
Subject: &block.TypeRange,
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ret := make([]*TestFileExpectDiag, 0, len(content.Blocks))
|
||||
for _, block := range content.Blocks {
|
||||
rng, remain, moreDiags := r.decodeRangeFromBody(block.Body)
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Should have nothing else in the block aside from the range definition.
|
||||
_, moreDiags = remain.Content(&hcl.BodySchema{})
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
var severity hcl.DiagnosticSeverity
|
||||
switch block.Type {
|
||||
case "error":
|
||||
severity = hcl.DiagError
|
||||
case "warning":
|
||||
severity = hcl.DiagWarning
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported block type %q", block.Type))
|
||||
}
|
||||
|
||||
ret = append(ret, &TestFileExpectDiag{
|
||||
Severity: severity,
|
||||
Range: rng,
|
||||
DeclRange: block.TypeRange,
|
||||
})
|
||||
}
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func (r *Runner) decodeRangeFromBody(body hcl.Body) (hcl.Range, hcl.Body, hcl.Diagnostics) {
|
||||
type RawPos struct {
|
||||
Line int `hcl:"line"`
|
||||
Column int `hcl:"column"`
|
||||
Byte int `hcl:"byte"`
|
||||
}
|
||||
type RawRange struct {
|
||||
From RawPos `hcl:"from,block"`
|
||||
To RawPos `hcl:"to,block"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
}
|
||||
|
||||
var raw RawRange
|
||||
diags := gohcl.DecodeBody(body, nil, &raw)
|
||||
|
||||
return hcl.Range{
|
||||
// We intentionally omit Filename here, because the test spec doesn't
|
||||
// need to specify that explicitly: we can infer it to be the file
|
||||
// path we pass to hcldec.
|
||||
Start: hcl.Pos{
|
||||
Line: raw.From.Line,
|
||||
Column: raw.From.Column,
|
||||
Byte: raw.From.Byte,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: raw.To.Line,
|
||||
Column: raw.To.Column,
|
||||
Byte: raw.To.Byte,
|
||||
},
|
||||
}, raw.Remain, diags
|
||||
}
|
||||
|
||||
var testFileSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "result",
|
||||
},
|
||||
{
|
||||
Name: "result_type",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "traversals",
|
||||
},
|
||||
{
|
||||
Type: "diagnostics",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testFileTraversalsSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "expect",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testFileTraversalExpectSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "ref",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "range",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testFileDiagnosticsSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "error",
|
||||
},
|
||||
{
|
||||
Type: "warning",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testFileRangeSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "from",
|
||||
},
|
||||
{
|
||||
Type: "to",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testFilePosSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "line",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "column",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "byte",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
117
cmd/hclspecsuite/traversals.go
Normal file
117
cmd/hclspecsuite/traversals.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func findTraversalSpec(got hcl.Traversal, candidates []*TestFileExpectTraversal) *TestFileExpectTraversal {
|
||||
for _, candidate := range candidates {
|
||||
if traversalsAreEquivalent(candidate.Traversal, got) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTraversalForSpec(want *TestFileExpectTraversal, have []hcl.Traversal) hcl.Traversal {
|
||||
for _, candidate := range have {
|
||||
if traversalsAreEquivalent(candidate, want.Traversal) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func traversalsAreEquivalent(a, b hcl.Traversal) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
aStep := a[i]
|
||||
bStep := b[i]
|
||||
|
||||
if reflect.TypeOf(aStep) != reflect.TypeOf(bStep) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We can now assume that both are of the same type.
|
||||
switch ts := aStep.(type) {
|
||||
|
||||
case hcl.TraverseRoot:
|
||||
if bStep.(hcl.TraverseRoot).Name != ts.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
case hcl.TraverseAttr:
|
||||
if bStep.(hcl.TraverseAttr).Name != ts.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
case hcl.TraverseIndex:
|
||||
if !bStep.(hcl.TraverseIndex).Key.RawEquals(ts.Key) {
|
||||
return false
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkTraversalsMatch determines if a given traversal matches the given
|
||||
// expectation, which must've been produced by an earlier call to
|
||||
// findTraversalSpec for the same traversal.
|
||||
func checkTraversalsMatch(got hcl.Traversal, filename string, match *TestFileExpectTraversal) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
gotRng := got.SourceRange()
|
||||
wantRng := match.Range
|
||||
|
||||
if got, want := gotRng.Filename, filename; got != want {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect filename in detected traversal",
|
||||
Detail: fmt.Sprintf(
|
||||
"Filename was reported as %q, but was expecting %q.",
|
||||
got, want,
|
||||
),
|
||||
Subject: match.Traversal.SourceRange().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
// If we have the expected filename then we'll use that to construct the
|
||||
// full "want range" here so that we can use it to point to the appropriate
|
||||
// location in the remaining diagnostics.
|
||||
wantRng.Filename = filename
|
||||
|
||||
if got, want := gotRng.Start, wantRng.Start; got != want {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect start position in detected traversal",
|
||||
Detail: fmt.Sprintf(
|
||||
"Start position was reported as line %d column %d byte %d, but was expecting line %d column %d byte %d.",
|
||||
got.Line, got.Column, got.Byte,
|
||||
want.Line, want.Column, want.Byte,
|
||||
),
|
||||
Subject: &wantRng,
|
||||
})
|
||||
}
|
||||
if got, want := gotRng.End, wantRng.End; got != want {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Incorrect end position in detected traversal",
|
||||
Detail: fmt.Sprintf(
|
||||
"End position was reported as line %d column %d byte %d, but was expecting line %d column %d byte %d.",
|
||||
got.Line, got.Column, got.Byte,
|
||||
want.Line, want.Column, want.Byte,
|
||||
),
|
||||
Subject: &wantRng,
|
||||
})
|
||||
}
|
||||
return diags
|
||||
}
|
1
hcl/spectests/.gitignore
vendored
Normal file
1
hcl/spectests/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
tmp_*
|
100
hcl/spectests/spec_test.go
Normal file
100
hcl/spectests/spec_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package spectests
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// The test harness is an external program that also expects to have
|
||||
// hcldec built as an external program, so we'll build both into
|
||||
// temporary files in our working directory before running our tests
|
||||
// here, to ensure that we're always running a build of the latest code.
|
||||
err := build()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Now we can run the tests
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func build() error {
|
||||
err := goBuild("github.com/hashicorp/hcl2/cmd/hcldec", "tmp_hcldec")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error building hcldec: %s", err)
|
||||
}
|
||||
|
||||
err = goBuild("github.com/hashicorp/hcl2/cmd/hclspecsuite", "tmp_hclspecsuite")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error building hcldec: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSpec(t *testing.T) {
|
||||
suiteDir := filepath.Clean("../../specsuite/tests")
|
||||
harness := "./tmp_hclspecsuite"
|
||||
hcldec := "./tmp_hcldec"
|
||||
|
||||
cmd := exec.Command(harness, suiteDir, hcldec)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if _, isExit := err.(*exec.ExitError); err != nil && !isExit {
|
||||
t.Errorf("failed to run harness: %s", err)
|
||||
}
|
||||
failed := err != nil
|
||||
|
||||
sc := bufio.NewScanner(bytes.NewReader(out))
|
||||
var lines []string
|
||||
for sc.Scan() {
|
||||
lines = append(lines, sc.Text())
|
||||
}
|
||||
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
cur := lines[i]
|
||||
if strings.HasPrefix(cur, "- ") {
|
||||
testName := cur[2:]
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
i++
|
||||
for i < len(lines) {
|
||||
cur := lines[i]
|
||||
if strings.HasPrefix(cur, "- ") || strings.HasPrefix(cur, "==") {
|
||||
return
|
||||
}
|
||||
t.Error(cur)
|
||||
i++
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if !strings.HasPrefix(cur, "==") { // not the "test harness problems" report, then
|
||||
t.Log(cur)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
t.Error("specsuite failed")
|
||||
}
|
||||
}
|
||||
|
||||
func goBuild(pkg, outFile string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
outFile += ".exe"
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "build", "-i", "-o", outFile, pkg)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
return cmd.Run()
|
||||
}
|
37
specsuite/README.md
Normal file
37
specsuite/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# HCL Language Test Suite
|
||||
|
||||
This directory contains an implementation-agnostic test suite that can be used
|
||||
to verify the correct behavior not only of the HCL implementation in _this_
|
||||
repository but also of possible other implementations.
|
||||
|
||||
The harness for running this suite -- a Go program in this directory -- uses
|
||||
the `hcldec` program as a level of abstraction to avoid depending directly on
|
||||
the Go implementation. As a result, other HCL implementations must also
|
||||
include a version of `hcldec` in order to run this spec.
|
||||
|
||||
The tests defined in this suite each correspond to a detail of
|
||||
[the HCL spec](../hcl/spec.md). This suite is separate from and not a
|
||||
substitute for direct unit tests of a given implementation that would presumably
|
||||
also exercise that implementation's own programmatic API.
|
||||
|
||||
To run the suite, first build the harness using Go:
|
||||
|
||||
```
|
||||
go install github.com/hashicorp/hcl2/cmd/hclspecsuite
|
||||
```
|
||||
|
||||
Then run it, passing it the directory containing the test definitions (the
|
||||
"tests" subdirectory of this directory) and the path to the `hcldec` executable
|
||||
to use.
|
||||
|
||||
For example, if working in the root of this repository and using the `hcldec`
|
||||
implementation from here:
|
||||
|
||||
```
|
||||
go install ./cmd/hcldec
|
||||
hclspecsuite ./specsuite/tests $GOPATH/bin/hcldec
|
||||
```
|
||||
|
||||
For developers working on the Go implementation of HCL from this repository,
|
||||
please note that this spec suite is run as part of a normal `go test ./...`
|
||||
execution for this whole repository and so does not need to be run separately.
|
1
specsuite/tests/comments/hash_comment.hcl
Normal file
1
specsuite/tests/comments/hash_comment.hcl
Normal file
@ -0,0 +1 @@
|
||||
# Hash comment
|
3
specsuite/tests/comments/hash_comment.hcldec
Normal file
3
specsuite/tests/comments/hash_comment.hcldec
Normal file
@ -0,0 +1,3 @@
|
||||
literal {
|
||||
value = "ok"
|
||||
}
|
4
specsuite/tests/comments/hash_comment.t
Normal file
4
specsuite/tests/comments/hash_comment.t
Normal file
@ -0,0 +1,4 @@
|
||||
# This test parses a file containing only a comment. It is a parsing-only test,
|
||||
# so the hcldec spec for this test is just a literal value given below.
|
||||
|
||||
result = "ok"
|
3
specsuite/tests/comments/multiline_comment.hcl
Normal file
3
specsuite/tests/comments/multiline_comment.hcl
Normal file
@ -0,0 +1,3 @@
|
||||
/*
|
||||
Multi-line comment
|
||||
*/
|
3
specsuite/tests/comments/multiline_comment.hcldec
Normal file
3
specsuite/tests/comments/multiline_comment.hcldec
Normal file
@ -0,0 +1,3 @@
|
||||
literal {
|
||||
value = "ok"
|
||||
}
|
4
specsuite/tests/comments/multiline_comment.t
Normal file
4
specsuite/tests/comments/multiline_comment.t
Normal file
@ -0,0 +1,4 @@
|
||||
# This test parses a file containing only a comment. It is a parsing-only test,
|
||||
# so the hcldec spec for this test is just a literal value given below.
|
||||
|
||||
result = "ok"
|
1
specsuite/tests/comments/slash_comment.hcl
Normal file
1
specsuite/tests/comments/slash_comment.hcl
Normal file
@ -0,0 +1 @@
|
||||
// Slash comment
|
3
specsuite/tests/comments/slash_comment.hcldec
Normal file
3
specsuite/tests/comments/slash_comment.hcldec
Normal file
@ -0,0 +1,3 @@
|
||||
literal {
|
||||
value = "ok"
|
||||
}
|
4
specsuite/tests/comments/slash_comment.t
Normal file
4
specsuite/tests/comments/slash_comment.t
Normal file
@ -0,0 +1,4 @@
|
||||
# This test parses a file containing only a comment. It is a parsing-only test,
|
||||
# so the hcldec spec for this test is just a literal value given below.
|
||||
|
||||
result = "ok"
|
0
specsuite/tests/empty.hcl
Normal file
0
specsuite/tests/empty.hcl
Normal file
1
specsuite/tests/empty.hcl.json
Normal file
1
specsuite/tests/empty.hcl.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
3
specsuite/tests/empty.hcldec
Normal file
3
specsuite/tests/empty.hcldec
Normal file
@ -0,0 +1,3 @@
|
||||
literal {
|
||||
value = "ok"
|
||||
}
|
9
specsuite/tests/empty.t
Normal file
9
specsuite/tests/empty.t
Normal file
@ -0,0 +1,9 @@
|
||||
# This test ensures that we can successfully parse an empty file.
|
||||
# Since an empty file has no content, the hcldec spec for this test is
|
||||
# just a literal value, which we test below.
|
||||
|
||||
result = "ok"
|
||||
|
||||
traversals {
|
||||
# Explicitly no traversals
|
||||
}
|
3
specsuite/tests/structure/attributes/expected.hcl
Normal file
3
specsuite/tests/structure/attributes/expected.hcl
Normal file
@ -0,0 +1,3 @@
|
||||
a = "a value"
|
||||
b = "b value"
|
||||
c = "c value"
|
11
specsuite/tests/structure/attributes/expected.hcldec
Normal file
11
specsuite/tests/structure/attributes/expected.hcldec
Normal file
@ -0,0 +1,11 @@
|
||||
object {
|
||||
attr "a" {
|
||||
type = string
|
||||
}
|
||||
attr "b" {
|
||||
type = string
|
||||
}
|
||||
attr "c" {
|
||||
type = string
|
||||
}
|
||||
}
|
10
specsuite/tests/structure/attributes/expected.t
Normal file
10
specsuite/tests/structure/attributes/expected.t
Normal file
@ -0,0 +1,10 @@
|
||||
result_type = object({
|
||||
a = string
|
||||
b = string
|
||||
c = string
|
||||
})
|
||||
result = {
|
||||
a = "a value"
|
||||
b = "b value"
|
||||
c = "c value"
|
||||
}
|
1
specsuite/tests/structure/attributes/singleline_bad.hcl
Normal file
1
specsuite/tests/structure/attributes/singleline_bad.hcl
Normal file
@ -0,0 +1 @@
|
||||
a = "a value", b = "b value"
|
@ -0,0 +1,3 @@
|
||||
literal {
|
||||
value = null
|
||||
}
|
19
specsuite/tests/structure/attributes/singleline_bad.t
Normal file
19
specsuite/tests/structure/attributes/singleline_bad.t
Normal file
@ -0,0 +1,19 @@
|
||||
# This test verifies that comma-separated attributes on the same line are
|
||||
# reported as an error, rather than being parsed like an object constructor
|
||||
# expression.
|
||||
|
||||
diagnostics {
|
||||
error {
|
||||
# Message like "missing newline after argument" or "each argument must be on its own line"
|
||||
from {
|
||||
line = 1
|
||||
column = 14
|
||||
byte = 13
|
||||
}
|
||||
to {
|
||||
line = 1
|
||||
column = 15
|
||||
byte = 14
|
||||
}
|
||||
}
|
||||
}
|
4
specsuite/tests/structure/attributes/unexpected.hcl
Normal file
4
specsuite/tests/structure/attributes/unexpected.hcl
Normal file
@ -0,0 +1,4 @@
|
||||
a = "a value"
|
||||
b = "b value"
|
||||
c = "c value"
|
||||
d = "d value"
|
11
specsuite/tests/structure/attributes/unexpected.hcldec
Normal file
11
specsuite/tests/structure/attributes/unexpected.hcldec
Normal file
@ -0,0 +1,11 @@
|
||||
object {
|
||||
attr "a" {
|
||||
type = string
|
||||
}
|
||||
attr "b" {
|
||||
type = string
|
||||
}
|
||||
attr "d" {
|
||||
type = string
|
||||
}
|
||||
}
|
15
specsuite/tests/structure/attributes/unexpected.t
Normal file
15
specsuite/tests/structure/attributes/unexpected.t
Normal file
@ -0,0 +1,15 @@
|
||||
diagnostics {
|
||||
error {
|
||||
# An argument named "c" is not expected here.
|
||||
from {
|
||||
line = 3
|
||||
column = 1
|
||||
byte = 28
|
||||
}
|
||||
to {
|
||||
line = 3
|
||||
column = 2
|
||||
byte = 29
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
a {}
|
@ -0,0 +1,4 @@
|
||||
block {
|
||||
block_type = "a"
|
||||
object {}
|
||||
}
|
1
specsuite/tests/structure/blocks/single_empty_oneline.t
Normal file
1
specsuite/tests/structure/blocks/single_empty_oneline.t
Normal file
@ -0,0 +1 @@
|
||||
result_type = object({})
|
3
specsuite/tests/structure/blocks/single_expected.hcl
Normal file
3
specsuite/tests/structure/blocks/single_expected.hcl
Normal file
@ -0,0 +1,3 @@
|
||||
a {
|
||||
|
||||
}
|
4
specsuite/tests/structure/blocks/single_expected.hcldec
Normal file
4
specsuite/tests/structure/blocks/single_expected.hcldec
Normal file
@ -0,0 +1,4 @@
|
||||
block {
|
||||
block_type = "a"
|
||||
object {}
|
||||
}
|
1
specsuite/tests/structure/blocks/single_expected.t
Normal file
1
specsuite/tests/structure/blocks/single_expected.t
Normal file
@ -0,0 +1 @@
|
||||
result_type = object({})
|
1
specsuite/tests/structure/blocks/single_unclosed.hcl
Normal file
1
specsuite/tests/structure/blocks/single_unclosed.hcl
Normal file
@ -0,0 +1 @@
|
||||
a {
|
4
specsuite/tests/structure/blocks/single_unclosed.hcldec
Normal file
4
specsuite/tests/structure/blocks/single_unclosed.hcldec
Normal file
@ -0,0 +1,4 @@
|
||||
block {
|
||||
block_type = "a"
|
||||
object {}
|
||||
}
|
14
specsuite/tests/structure/blocks/single_unclosed.t
Normal file
14
specsuite/tests/structure/blocks/single_unclosed.t
Normal file
@ -0,0 +1,14 @@
|
||||
diagnostics {
|
||||
error {
|
||||
from {
|
||||
line = 2
|
||||
column = 1
|
||||
byte = 4
|
||||
}
|
||||
to {
|
||||
line = 2
|
||||
column = 1
|
||||
byte = 4
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user