mirror of
https://github.com/lukaszraczylo/llm-testbench.git
synced 2026-10-13 00:48:29 +00:00
feat(cli): self-updating progress counter
The progress stream now shows a single rewriting "test N of M" line on a terminal, instead of one detail line per request. A piped stderr gets a plain line every 25 completions plus the final one.
This commit is contained in:
+47
-28
@@ -3,22 +3,48 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/lukaszraczylo/llm-testbench/internal/runner"
|
||||
)
|
||||
|
||||
// stderrProgressReporter implements runner.ProgressReporter, writing one
|
||||
// line per event to w (os.Stderr in production). Run's stdout stays
|
||||
// pipeable (table/markdown/json only): all progress goes here instead.
|
||||
// pipedProgressEvery is how often a non-terminal stderr gets a progress
|
||||
// line. A piped log stays readable without one line per request.
|
||||
const pipedProgressEvery = 25
|
||||
|
||||
// stderrProgressReporter implements runner.ProgressReporter, writing a
|
||||
// self-updating "test N of M" counter to w (os.Stderr in production).
|
||||
// Run's stdout stays pipeable (table/markdown/json only): all progress
|
||||
// goes here instead.
|
||||
//
|
||||
// When w is a terminal the counter rewrites itself in place with a
|
||||
// carriage return. When w is piped (a log file), rewriting would produce
|
||||
// one unreadable blob, so it prints a plain line every pipedProgressEvery
|
||||
// completions plus the final one.
|
||||
type stderrProgressReporter struct {
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
isTTY bool
|
||||
}
|
||||
|
||||
// newStderrProgressReporter builds a stderrProgressReporter writing to w.
|
||||
func newStderrProgressReporter(w io.Writer) *stderrProgressReporter {
|
||||
return &stderrProgressReporter{w: w}
|
||||
return &stderrProgressReporter{w: w, isTTY: isTerminal(w)}
|
||||
}
|
||||
|
||||
// isTerminal reports whether w is a character device (an interactive
|
||||
// terminal), using only the standard library.
|
||||
func isTerminal(w io.Writer) bool {
|
||||
f, ok := w.(*os.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// ReportStart implements runner.ProgressReporter.
|
||||
@@ -34,30 +60,23 @@ func (r *stderrProgressReporter) ReportStart(totalTests, totalModels, concurrenc
|
||||
|
||||
// ReportDone implements runner.ProgressReporter. Safe for concurrent calls
|
||||
// from up to Config.Concurrency goroutines at once: the mutex serializes
|
||||
// each printed line so concurrent completions cannot interleave mid-line.
|
||||
func (r *stderrProgressReporter) ReportDone(done, total int, result runner.Result) {
|
||||
status := resultStatus(result)
|
||||
truncated := ""
|
||||
if result.Truncated() {
|
||||
truncated = " TRUNCATED"
|
||||
}
|
||||
|
||||
// writes so concurrent completions cannot interleave mid-line.
|
||||
func (r *stderrProgressReporter) ReportDone(done, total int, _ runner.Result) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
// See ReportStart: a write failure here is not actionable.
|
||||
_, _ = fmt.Fprintf(r.w, "[%d/%d] model=%s test=%s score=%s tokens=%d (%s)%s\n",
|
||||
done, total, result.Model, result.TestID, status, result.TotalTokens(), result.Latency, truncated)
|
||||
}
|
||||
|
||||
// resultStatus renders a Result's score for one progress line: the numeric
|
||||
// score, or ERR/skip.
|
||||
func resultStatus(result runner.Result) string {
|
||||
switch {
|
||||
case result.Err != nil:
|
||||
return "ERR"
|
||||
case result.Score.Skipped:
|
||||
return "skip"
|
||||
default:
|
||||
return fmt.Sprintf("%.2f", result.Score.Value)
|
||||
if r.isTTY {
|
||||
// \r returns to the line start, \x1b[K clears any longer previous
|
||||
// text; the final completion ends the line so later output starts
|
||||
// clean.
|
||||
_, _ = fmt.Fprintf(r.w, "\rtest %d of %d\x1b[K", done, total)
|
||||
if done == total {
|
||||
_, _ = fmt.Fprintln(r.w)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if done%pipedProgressEvery == 0 || done == total {
|
||||
_, _ = fmt.Fprintf(r.w, "test %d of %d\n", done, total)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user