Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions cmd/pipelines/test_render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package pipelines

import (
"fmt"
"io"
"strings"
"unicode/utf8"

"github.com/databricks/databricks-sdk-go/service/pipelines"
)

// testLineWidth is the width to which a case line is padded with dots before its status. The
// fixed width lines up the statuses while each case prints as it finishes.
const testLineWidth = 80

// testProgressPrinter prints the phase of a test update and each case as it finishes.
type testProgressPrinter struct {
out io.Writer
shortID func(id string) string
finished int
waiting bool
running bool
}

func (p *testProgressPrinter) updateState(state pipelines.UpdateInfoState) {
switch state {
case pipelines.UpdateInfoStateQueued,
pipelines.UpdateInfoStateCreated,
pipelines.UpdateInfoStateWaitingForResources,
pipelines.UpdateInfoStateInitializing,
pipelines.UpdateInfoStateResetting,
pipelines.UpdateInfoStateSettingUpTables:
if !p.waiting && !p.running {
fmt.Fprintln(p.out, "Waiting for compute...")
p.waiting = true
}
case pipelines.UpdateInfoStateRunning:
p.startRunning()
default:
}
}

func (p *testProgressPrinter) startRunning() {
if !p.running {
fmt.Fprintln(p.out, "Running tests...")
p.running = true
}
}

// caseFinished prints a line such as
// "2 of 3 FAILED tests/test_orders.py::test_backfill ...... [FAILED in 2.14s]". total is the
// number of known cases; the service reports every case as PENDING before the first one runs.
func (p *testProgressPrinter) caseFinished(c *testCaseResult, total int) {
p.startRunning()
p.finished++
prefix := fmt.Sprintf("%d of %d %s %s ", p.finished, total, c.status, p.shortID(c.id))
status := string(c.status)
if d, ok := c.duration(); ok {
status += fmt.Sprintf(" in %.2fs", d.Seconds())
}
dots := strings.Repeat(".", max(testLineWidth-utf8.RuneCountInString(prefix), 0))
fmt.Fprintf(p.out, "%s%s [%s]\n", prefix, dots, status)
}

// renderTestResult prints the end of a test run, before its summary line: the details of every
// failed or errored case, then why the result is incomplete or that no tests were collected.
func renderTestResult(out io.Writer, run *testRun, incompleteReason string, shortID func(id string) string) {
fmt.Fprintln(out)
counts := run.counts()
if counts.Failed > 0 || counts.Errored > 0 {
fmt.Fprintf(out, "Completed with %d failed and %d errored tests:\n", counts.Failed, counts.Errored)
for _, c := range run.cases {
var label string
switch c.status {
case testCaseFailed:
label = "Failure in"
case testCaseError:
label = "Error in"
default:
continue
}
fmt.Fprintf(out, "\n%s %s\n", label, shortID(c.id))
details := ""
if c.errorDetail != nil {
details = errorDetailText(c.errorDetail)
}
if details == "" {
details = "No details were reported."
}
for line := range strings.SplitSeq(details, "\n") {
fmt.Fprintf(out, " %s\n", line)
}
if c.detailsTruncated {
fmt.Fprintln(out, " (details truncated)")
}
}
fmt.Fprintln(out)
}

switch {
case incompleteReason != "":
fmt.Fprintf(out, "RUN INCOMPLETE: %s.\n", incompleteReason)
case run.summaries[0].Total == 0:
fmt.Fprintln(out, "No tests were collected.")
}
}

// renderTestSummaryLine prints the last line of a test run, "Done. PASSED=… TOTAL=…".
// A complete run shows the service's summary. An incomplete run shows the results received, and
// its total counts every case the events named.
func renderTestSummaryLine(out io.Writer, run *testRun, incompleteReason string) {
s := run.counts()
s.Total = len(run.cases)
if incompleteReason == "" {
s = run.summaries[0]
}
fmt.Fprintf(out, "Done. PASSED=%d FAILED=%d SKIPPED=%d ERROR=%d TOTAL=%d\n", s.Passed, s.Failed, s.Skipped, s.Errored, s.Total)
}
80 changes: 80 additions & 0 deletions cmd/pipelines/test_render_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package pipelines

import (
"bytes"
"strings"
"testing"

"github.com/databricks/databricks-sdk-go/service/pipelines"
"github.com/stretchr/testify/assert"
)

func shortTestID(id string) string {
return strings.TrimPrefix(id, "/Workspace/p/")
}

func TestTestProgressPrinter(t *testing.T) {
var out bytes.Buffer
p := &testProgressPrinter{out: &out, shortID: shortTestID}
p.updateState(pipelines.UpdateInfoStateWaitingForResources)
p.updateState(pipelines.UpdateInfoStateInitializing)
p.updateState(pipelines.UpdateInfoStateRunning)

ev := newTestEvents(t)
run := newTestRun()
run.add(ev.testCase("/Workspace/p/tests/test_orders.py::test_backfill", "RUNNING", "2026-01-01T00:00:00.000Z"))
p.caseFinished(run.add(ev.testCase("/Workspace/p/tests/test_orders.py::test_backfill", "FAILED", "2026-01-01T00:00:02.140Z")), 2)
long := "/Workspace/p/tests/" + strings.Repeat("x", 70) + ".py::test_y"
p.caseFinished(run.add(ev.testCase(long, "SKIPPED", "")), 2)

assert.Equal(t, `Waiting for compute...
Running tests...
1 of 2 FAILED tests/test_orders.py::test_backfill .............................. [FAILED in 2.14s]
2 of 2 SKIPPED `+shortTestID(long)+` [SKIPPED]
`, out.String())
}

func TestRenderTestResult(t *testing.T) {
ev := newTestEvents(t)
render := func(run *testRun, incompleteReason string) string {
var out bytes.Buffer
renderTestResult(&out, run, incompleteReason, shortTestID)
renderTestSummaryLine(&out, run, incompleteReason)
return out.String()
}

failed := newTestRun()
failed.add(ev.testCase("/Workspace/p/tests/test_a.py::test_ok", "PASSED", ""))
failed.add(ev.testCase("/Workspace/p/tests/test_a.py::test_bad", "FAILED", "",
`"error": {"exceptions": [{"class_name": "AssertionError", "message": "assert 1 == 2"}]}`,
`"truncation": {"truncated_fields": [{"field_name": "error"}]}`))
failed.add(ev.testCase("/Workspace/p/tests/test_a.py::test_fixture", "ERROR", ""))
failed.add(ev.summary(3, 1, 1, 0, 1))
assert.Equal(t, `
Completed with 1 failed and 1 errored tests:

Failure in tests/test_a.py::test_bad
AssertionError: assert 1 == 2
(details truncated)

Error in tests/test_a.py::test_fixture
No details were reported.

Done. PASSED=1 FAILED=1 SKIPPED=0 ERROR=1 TOTAL=3
`, render(failed, ""))

incomplete := newTestRun()
incomplete.add(ev.testCase("/Workspace/p/tests/test_b.py::test_one", "PASSED", ""))
incomplete.add(ev.testCase("/Workspace/p/tests/test_b.py::test_two", "PENDING", ""))
assert.Equal(t, `
RUN INCOMPLETE: the update failed before all test results were received.
Done. PASSED=1 FAILED=0 SKIPPED=0 ERROR=0 TOTAL=2
`, render(incomplete, "the update failed before all test results were received"))

empty := newTestRun()
empty.add(ev.summary(0, 0, 0, 0, 0))
assert.Equal(t, `
No tests were collected.
Done. PASSED=0 FAILED=0 SKIPPED=0 ERROR=0 TOTAL=0
`, render(empty, ""))
}
Loading