diff --git a/README.md b/README.md index 9f49704..6dd8d00 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ More information can be found in the [TODO] blog post. ## Installation -``` +```bash go install -u github.com/roblaszczak/vgt ``` @@ -22,29 +22,38 @@ You can also run without installing by running `go run github.com/roblaszczak/vg For visualising test results, run `go test` with the `-json` flag and pipe the output to `vgt`. -``` +```bash go test -json ./... | vgt ``` or with `go run`: -``` +```bash go test -json ./... | go run github.com/roblaszczak/vgt@latest ``` +> [!WARNING] +> When you are piping go tests output to `vgt`, `vgt` will exit with 1 when tests failed. + +or just `vgt` with a custom flags after `--` to run tests and visualise them: + +```bash +$ vgt -- ./... -count=1 -short +10:26PM INF Running go test command="[go test -json ./... -count=1 -short]" +``` + After tests were executed, a browser window will open with the visualisation. If you want to preserve the output, you can pipe test logs to file and later pass it to `vgt`: -``` +```bash go test -json ./... > test.json cat test.json | vgt ``` - ### Additional flags -``` +```bash Usage of vgt: -debug enable debug mode @@ -75,6 +84,6 @@ it looks good. If you made a change and want to update golden files, you can run: -``` +```bash go test . -update-golden ``` diff --git a/main.go b/main.go index 91a0236..7a88063 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,16 @@ package main import ( "bufio" + "bytes" "context" _ "embed" + "errors" "flag" "fmt" "io" "log/slog" "os" + "os/exec" "os/signal" "syscall" "time" @@ -47,7 +50,7 @@ func main() { ) flag.Parse() - logLevel := slog.LevelWarn + logLevel := slog.LevelInfo if debug { logLevel = slog.LevelDebug } @@ -65,33 +68,11 @@ func main() { }), )) - var r io.Reader - - if fromFile == "" { - sr, err := cancelreader.NewReader(os.Stdin) - if err != nil { - slog.Error("Error creating cancel reader", "err", err) - return - } - - go func() { - <-ctx.Done() - sr.Cancel() - }() - - r = sr - } else { - f, err := os.Open(fromFile) - if err != nil { - slog.Error("Error opening file", "err", err) - return - } - defer func() { - _ = f.Close() - }() - - r = f + r, cleanup, exitCode, done := newReader(ctx) + if !done { + return } + defer cleanup() scanner := bufio.NewScanner(r) @@ -112,6 +93,91 @@ func main() { } else { serveHTML(ctx, result) } + + if exitCode != 0 { + os.Exit(exitCode) + } + if result.Failed { + os.Exit(1) + } +} + +func newReader(ctx context.Context) (io.Reader, func(), int, bool) { + fi, err := os.Stdin.Stat() + if err != nil { + slog.Error("Error getting stdin stat", "err", err) + return nil, nil, 0, false + } + + isPipe := (fi.Mode() & os.ModeCharDevice) == 0 + readFromFile := fromFile != "" + + if isPipe && readFromFile { + slog.Error("Can't read from file and stdin at the same time") + return nil, nil, 0, false + } + + if readFromFile { + f, err := os.Open(fromFile) + if err != nil { + slog.Error("Error opening file", "err", err) + return nil, nil, 0, false + } + + return f, func() { + _ = f.Close() + }, 0, true + } + + if isPipe { + sr, err := cancelreader.NewReader(os.Stdin) + if err != nil { + slog.Error("Error creating cancel reader", "err", err) + return nil, nil, 0, false + } + + go func() { + <-ctx.Done() + sr.Cancel() + }() + + return sr, func() {}, 0, true + } + + r := bytes.NewBuffer([]byte{}) + + command := append([]string{"go", "test", "-json"}, flag.Args()...) + + slog.Info("Running go test", "command", command) + + cmd := exec.Command(command[0], command[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = io.MultiWriter(r, os.Stdout) + cmd.Stderr = os.Stderr + + var exitCode int + + err = cmd.Run() + var exitErr *exec.ExitError + if err != nil { + if errors.As(err, &exitErr) { + // this is expected - tests failed + slog.Info("Error running go test", "err", err) + exitCode = exitErr.ExitCode() + } else { + slog.Error("Error running go test", "err", err) + return nil, nil, 0, false + } + } + + go func() { + <-ctx.Done() + _ = cmd.Process.Kill() + }() + + return r, func() { + _, _ = cmd.Process.Wait() + }, exitCode, true } func checkClosing(ctx context.Context) bool { diff --git a/parser.go b/parser.go index 3c15046..1c9c6de 100644 --- a/parser.go +++ b/parser.go @@ -102,6 +102,8 @@ type ParseResult struct { End time.Time MaxDuration time.Duration + + Failed bool } func (p ParseResult) TestNamesOrderedByStart() []TestName { @@ -133,6 +135,8 @@ func Parse(scanner *bufio.Scanner) ParseResult { maxDuration := time.Duration(0) + failed := false + i := 0 for scanner.Scan() { @@ -157,6 +161,10 @@ func Parse(scanner *bufio.Scanner) ParseResult { continue } + if out.Action == actionFail { + failed = true + } + if !out.Time.IsZero() { if start.IsZero() || out.Time.Before(start) { start = out.Time @@ -268,5 +276,6 @@ func Parse(scanner *bufio.Scanner) ParseResult { Start: start, End: end, MaxDuration: maxDuration, + Failed: failed, } } diff --git a/testdata/golden.json b/testdata/golden.json index a101fc4..c4baa9a 100644 --- a/testdata/golden.json +++ b/testdata/golden.json @@ -4046,5 +4046,6 @@ }, "Start": "2024-09-18T21:02:11.282888+02:00", "End": "2024-09-18T21:12:13.0365+02:00", - "MaxDuration": 262054805000 + "MaxDuration": 262054805000, + "Failed": true } \ No newline at end of file