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 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 }