Skip to content

Commit

Permalink
cmd/hclspecsuite: Check for expected diagnostics
Browse files Browse the repository at this point in the history
When a test file declares one or more expected diagnostics, we check those
instead of checking the result value. The severities and source ranges
must match.

We don't test the error messages themselves because they are not part of
the specification and may vary between implementations or, in future, be
translated into other languages.
  • Loading branch information
apparentlymart committed Aug 12, 2018
1 parent 767fb36 commit a5c0f7f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 28 deletions.
19 changes: 19 additions & 0 deletions cmd/hclspecsuite/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,22 @@ func decodeJSONDiagnostics(src []byte) hcl.Diagnostics {

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,
)
}
126 changes: 98 additions & 28 deletions cmd/hclspecsuite/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,44 +169,114 @@ func (r *Runner) runTestInput(specFilename, inputFilename string, tf *TestFile)

}

val, moreDiags := r.hcldecTransform(specFilename, inputFilename)
diags = append(diags, moreDiags...)
if moreDiags.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),
),
})
}
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 tf.Result != cty.NilVal {
cmpVal, err := convert.Convert(tf.Result, tf.ResultType)
if err != nil {
if errs := val.Type().TestConformance(tf.ResultType); len(errs) > 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Incorrect type for result value",
Summary: "Incorrect result type",
Detail: fmt.Sprintf(
"Result does not conform to the given result type: %s.", err,
"Input file %s produced %s, but was expecting %s.",
inputFilename, typeexpr.TypeString(val.Type()), typeexpr.TypeString(tf.ResultType),
),
Subject: &tf.ResultRange,
})
} else {
if !val.RawEquals(cmpVal) {
}

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: "Incorrect result value",
Summary: "Missing expected diagnostic",
Detail: fmt.Sprintf(
"Input file %s produced %#v, but was expecting %#v.",
inputFilename, val, tf.Result,
"No %s diagnostic was generated %s.",
severityString(wantEntry.Severity), rangeString(wantEntry.Range),
),
Subject: &declRange,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions specsuite/tests/structure/attributes/singleline_bad.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a = "a value", b = "b value"
3 changes: 3 additions & 0 deletions specsuite/tests/structure/attributes/singleline_bad.hcldec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
literal {
value = null
}
19 changes: 19 additions & 0 deletions specsuite/tests/structure/attributes/singleline_bad.t
Original file line number Diff line number Diff line change
@@ -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
}
}
}

0 comments on commit a5c0f7f

Please sign in to comment.