Files
ConfigKit/diagnostic_test.go

144 lines
4.0 KiB
Go

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
}