Add semantic paths and safe diagnostics
This commit is contained in:
143
diagnostic_test.go
Normal file
143
diagnostic_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiagnosticsAreCanonicalAndDetached(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
diagnostics := []Diagnostic{
|
||||
{
|
||||
Code: "decode.type",
|
||||
Severity: SeverityError,
|
||||
Path: Keys("later"),
|
||||
Message: "later",
|
||||
Primary: &Location{SourceID: "source-10", Line: 1, Column: 1},
|
||||
},
|
||||
{
|
||||
Code: "schema.invalid",
|
||||
Severity: SeverityError,
|
||||
Path: Keys("root"),
|
||||
Message: "source-less",
|
||||
},
|
||||
{
|
||||
Code: "decode.type",
|
||||
Severity: SeverityError,
|
||||
Path: Keys("earlier"),
|
||||
Message: "earlier",
|
||||
Primary: &Location{SourceID: "source-2", Line: 3, Column: 4},
|
||||
Related: []Location{
|
||||
{SourceID: "source-10", Line: 1, Column: 1},
|
||||
{SourceID: "source-2", Line: 9, Column: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var forward bytes.Buffer
|
||||
if err := WriteDiagnostics(&forward, diagnostics); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reversed := []Diagnostic{diagnostics[2], diagnostics[1], diagnostics[0]}
|
||||
var backward bytes.Buffer
|
||||
if err := WriteDiagnostics(&backward, reversed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if forward.String() != backward.String() {
|
||||
t.Fatalf("rendering depends on submission order:\n%s\n---\n%s", forward.String(), backward.String())
|
||||
}
|
||||
lines := strings.Split(forward.String(), "\n")
|
||||
if !strings.Contains(lines[0], "source-less") || !strings.Contains(lines[1], "source-2:3:4") {
|
||||
t.Fatalf("unexpected canonical order:\n%s", forward.String())
|
||||
}
|
||||
if !strings.Contains(lines[2], "source-2:9:1") || !strings.Contains(lines[3], "source-10:1:1") {
|
||||
t.Fatalf("related locations not canonical:\n%s", forward.String())
|
||||
}
|
||||
|
||||
accumulator := diagnosticAccumulator{
|
||||
sources: []SourceInfo{{
|
||||
ID: "source-1",
|
||||
Label: "config.yml",
|
||||
ImportedFrom: &Location{SourceID: "source-0", Line: 1},
|
||||
}},
|
||||
}
|
||||
accumulator.add(diagnostics...)
|
||||
loadErr := accumulator.loadError()
|
||||
var found *LoadError
|
||||
if !errors.As(error(loadErr), &found) || found != loadErr {
|
||||
t.Fatal("errors.As did not find LoadError")
|
||||
}
|
||||
|
||||
first := loadErr.Diagnostics()
|
||||
first[1].Primary.SourceID = "mutated"
|
||||
first[1].Path.segments[0].key = "mutated"
|
||||
first[1].Related[0].SourceID = "mutated"
|
||||
second := loadErr.Diagnostics()
|
||||
if second[1].Primary.SourceID != "source-2" || second[1].Path.String() != "/earlier" || second[1].Related[0].SourceID != "source-2" {
|
||||
t.Fatal("Diagnostics returned aliased records")
|
||||
}
|
||||
sources := loadErr.Sources()
|
||||
sources[0].ImportedFrom.SourceID = "mutated"
|
||||
if loadErr.Sources()[0].ImportedFrom.SourceID != "source-0" {
|
||||
t.Fatal("Sources returned aliased records")
|
||||
}
|
||||
if got := loadErr.Error(); got != "configkit: configuration load failed with 3 errors" {
|
||||
t.Fatalf("Error() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDiagnosticsEscapesControls(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
diagnostic := Diagnostic{
|
||||
Code: "validate.error",
|
||||
Severity: SeverityError,
|
||||
Path: Keys("line\nbreak"),
|
||||
Message: "message\r\nnext",
|
||||
Primary: &Location{SourceID: "source\tlabel", Line: 1},
|
||||
}
|
||||
var output bytes.Buffer
|
||||
if err := WriteDiagnostics(&output, []Diagnostic{diagnostic}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := output.String()
|
||||
if strings.Count(got, "\n") != 1 {
|
||||
t.Fatalf("control characters created output lines: %q", got)
|
||||
}
|
||||
for _, escaped := range []string{`line\nbreak`, `message\r\nnext`, `source\tlabel`} {
|
||||
if !strings.Contains(got, escaped) {
|
||||
t.Errorf("output %q does not contain %q", got, escaped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDiagnosticsPropagatesWriterFailures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
diagnostics := []Diagnostic{{Severity: SeverityError, Message: "safe"}}
|
||||
want := errors.New("write failed")
|
||||
if err := WriteDiagnostics(errorWriter{err: want}, diagnostics); !errors.Is(err, want) {
|
||||
t.Fatalf("error writer returned %v", err)
|
||||
}
|
||||
if err := WriteDiagnostics(shortWriter{}, diagnostics); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (w errorWriter) Write(p []byte) (int, error) {
|
||||
return min(3, len(p)), w.err
|
||||
}
|
||||
|
||||
type shortWriter struct{}
|
||||
|
||||
func (shortWriter) Write(p []byte) (int, error) {
|
||||
return max(0, len(p)-1), nil
|
||||
}
|
||||
Reference in New Issue
Block a user