164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
package engine
|
|
|
|
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 TestCanonicalDiagnosticsSortEscapedPaths(t *testing.T) {
|
|
primary := &Location{SourceID: "source-1", Line: 1, Column: 1}
|
|
diagnostics := []Diagnostic{
|
|
{Code: "same", Severity: SeverityError, Path: Keys("a/b"), Message: "same", Primary: primary},
|
|
{Code: "same", Severity: SeverityError, Path: Keys("a~b"), Message: "same", Primary: primary},
|
|
{Code: "z", Severity: SeverityError, Path: Keys("same"), Message: "same", Primary: primary},
|
|
{Code: "a", Severity: SeverityError, Path: Keys("same"), Message: "same", Primary: primary},
|
|
}
|
|
canonical := canonicalDiagnostics(diagnostics)
|
|
want := []string{"/a~0b", "/a~1b", "/same", "/same"}
|
|
for i, diagnostic := range canonical {
|
|
if got := diagnostic.Path.String(); got != want[i] {
|
|
t.Fatalf("diagnostic[%d] path = %q, want %q", i, got, want[i])
|
|
}
|
|
}
|
|
if canonical[2].Code != "a" || canonical[3].Code != "z" {
|
|
t.Fatalf("same-path code order = %q, %q", canonical[2].Code, canonical[3].Code)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|