specsuite: Start of the harness for the specification test suite
This commit is contained in:
parent
6743a2254b
commit
0956c193b7
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).
|
3
cmd/hclspecsuite/log.go
Normal file
3
cmd/hclspecsuite/log.go
Normal file
@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
type LogCallback func(testName string, testFile *TestFile)
|
58
cmd/hclspecsuite/main.go
Normal file
58
cmd/hclspecsuite/main.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/hashicorp/hcl2/hclparse"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
runner := &Runner{
|
||||
parser: parser,
|
||||
hcldecPath: hcldecPath,
|
||||
baseDir: testsDir,
|
||||
log: func(name string, file *TestFile) {
|
||||
fmt.Printf("- %s\n", name)
|
||||
},
|
||||
}
|
||||
diags := runner.Run()
|
||||
|
||||
if len(diags) != 0 {
|
||||
os.Stderr.WriteString("\n")
|
||||
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)
|
||||
diagWr.WriteDiagnostics(diags)
|
||||
return 2
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
164
cmd/hclspecsuite/runner.go
Normal file
164
cmd/hclspecsuite/runner.go
Normal file
@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hclparse"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
parser *hclparse.Parser
|
||||
hcldecPath string
|
||||
baseDir string
|
||||
log LogCallback
|
||||
}
|
||||
|
||||
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(r.baseDir, filename)
|
||||
testDiags := r.runTest(filename)
|
||||
diags = append(diags, testDiags...)
|
||||
}
|
||||
|
||||
for _, dirName := range subDirs {
|
||||
dir := filepath.Join(r.baseDir, 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.log != nil {
|
||||
r.log(prettyName, nil)
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
if r.log != nil {
|
||||
r.log(prettyName, tf)
|
||||
}
|
||||
|
||||
basePath := filename[:len(filename)-2]
|
||||
specFilename := basePath + ".hcldec"
|
||||
nativeFilename := basePath + ".hcl"
|
||||
//jsonFilename := basePath + ".hcl.json"
|
||||
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
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{
|
||||
"--spec=" + specFile,
|
||||
"--diags=json",
|
||||
inputFile,
|
||||
},
|
||||
Stdout: &outBuffer,
|
||||
Stderr: &errBuffer,
|
||||
}
|
||||
err := cmd.Run()
|
||||
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 err != nil {
|
||||
// If we exited unsuccessfully then we'll expect diagnostics on stderr
|
||||
// TODO: implement that
|
||||
} else {
|
||||
// Otherwise, we expect a JSON result value on stdout
|
||||
// TODO: implement that
|
||||
}
|
||||
|
||||
return cty.DynamicVal, 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)
|
||||
}
|
78
cmd/hclspecsuite/test_file.go
Normal file
78
cmd/hclspecsuite/test_file.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
type TestFile struct {
|
||||
Result cty.Value
|
||||
ResultType cty.Type
|
||||
|
||||
Traversals []hcl.Traversal
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
var testFileSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "result",
|
||||
},
|
||||
{
|
||||
Name: "result_type",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "traversals",
|
||||
},
|
||||
},
|
||||
}
|
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.
|
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user