pax_global_header 0000666 0000000 0000000 00000000064 15052652102 0014510 g ustar 00root root 0000000 0000000 52 comment=ba2512e7be150bfcbd6f6220d517d3741f8f2f75
tparse-0.18.0/ 0000775 0000000 0000000 00000000000 15052652102 0013074 5 ustar 00root root 0000000 0000000 tparse-0.18.0/.github/ 0000775 0000000 0000000 00000000000 15052652102 0014434 5 ustar 00root root 0000000 0000000 tparse-0.18.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15052652102 0016471 5 ustar 00root root 0000000 0000000 tparse-0.18.0/.github/workflows/ci.yaml 0000664 0000000 0000000 00000003535 15052652102 0017756 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
strategy:
matrix:
# go-version: ['oldstable', 'stable', '1.23.0-rc.2']
go-version: ['oldstable', 'stable']
env:
VERBOSE: 1
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Build
run: go build -v .
- name: Run tests with GITHUB_STEP_SUMMARY
shell: bash
# Note the use of || true. This so the job doesn't fail at that line. We want to preserve -follow
# as part of the test output, but not output it to the summary page, which is done in the proceeding
# command when we parse the output.jsonl file.
run: |
go test -v -count=1 -race ./... -json -coverpkg github.com/mfridman/tparse/parse \
| tee output.jsonl | ./tparse -notests -follow -all || true
./tparse -format markdown -file output.jsonl -all -slow 20 > $GITHUB_STEP_SUMMARY
- name: Run tparse w/ std lib
run: go test -count=1 fmt strings bytes bufio crypto log mime sort slices -json -cover | ./tparse -follow -all
- name: Install GoReleaser
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable'
uses: goreleaser/goreleaser-action@v6
with:
install-only: true
distribution: goreleaser
version: "~> v2"
- name: Gorelease dry-run
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable'
run: |
goreleaser release --skip=publish --snapshot --fail-fast --clean
tparse-0.18.0/.github/workflows/lint.yaml 0000664 0000000 0000000 00000001142 15052652102 0020321 0 ustar 00root root 0000000 0000000 name: golangci
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: golangci-lint
uses: golangci/golangci-lint-action@v5
with:
version: latest
github-token: ${{ secrets.GITHUB_TOKEN }}
args: --timeout=2m --verbose
annotations: false
tparse-0.18.0/.github/workflows/release.yaml 0000664 0000000 0000000 00000001434 15052652102 0020777 0 ustar 00root root 0000000 0000000 name: goreleaser
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Generate release notes
continue-on-error: true
run: ./scripts/release-notes.sh ${{github.ref_name}} > ${{runner.temp}}/release_notes.txt
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: release --clean --release-notes=${{runner.temp}}/release_notes.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tparse-0.18.0/.golangci.yaml 0000664 0000000 0000000 00000006222 15052652102 0015623 0 ustar 00root root 0000000 0000000 ---
linters:
enable:
# check when errors are compared without errors.Is
- errorlint
# check imports order and makes it always deterministic.
- gci
# Very Basic spell error checker
- misspell
# Fast, configurable, extensible, flexible, and beautiful linter for Go.
# Drop-in replacement of golint.
- revive
# make sure to use t.Helper() when needed
- thelper
# ensure that lint exceptions have explanations. Consider the case below:
- nolintlint
# detect duplicated words in code
- dupword
# mirror suggests rewrites to avoid unnecessary []byte/string conversion
- mirror
# testify checks good usage of github.com/stretchr/testify.
- testifylint
linters-settings:
dupword:
# Keywords used to ignore detection.
# Default: []
ignore:
- "FAIL" # "FAIL FAIL" is tolerated
nolintlint:
# Disable to ensure that all nolint directives actually have an effect.
# Default: false
allow-unused: true # too many false positive reported
# Exclude following linters from requiring an explanation.
# Default: []
allow-no-explanation: []
# Enable to require an explanation of nonzero length
# after each nolint directive.
# Default: false
require-explanation: true
# Enable to require nolint directives to mention the specific
# linter being suppressed.
# Default: false
require-specific: true
revive:
rules:
- name: bare-return
- name: blank-imports
- name: comment-spacings
- name: context-as-argument
arguments:
- allowTypesBefore: "*testing.T"
- name: context-keys-type
- name: defer
arguments:
- ["call-chain", "loop"]
- name: dot-imports
- name: early-return
- name: empty-block
- name: error-return
- name: error-strings
- name: error-naming
- name: errorf
- name: exported
arguments:
# enables checking public methods of private types
- "checkPrivateReceivers"
# make error messages clearer
- "sayRepetitiveInsteadOfStutters"
- name: if-return
- name: import-shadowing
- name: increment-decrement
- name: indent-error-flow
- name: exported
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: time-equal
- name: unexported-return
- name: use-any
- name: unreachable-code
- name: unhandled-error
arguments:
- "fmt.Print.*"
- "fmt.Fprint.*"
- "bytes.Buffer.Write.*"
- "strings.Builder.Write.*"
- name: unused-parameter
- name: unused-receiver
- name: useless-break
# define the import orders
gci:
sections:
# Standard section: captures all standard packages.
- standard
# Default section: catchall that is not standard or custom
- default
# Custom section: groups all imports with the specified Prefix.
- prefix(github.com/mfridman)
tparse-0.18.0/.goreleaser.yaml 0000664 0000000 0000000 00000001571 15052652102 0016172 0 ustar 00root root 0000000 0000000 # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
#
# See https://goreleaser.com/customization/ for more information.
version: 2
project_name: tparse
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
binary: tparse
main: main.go
goos:
- linux
- darwin
# - windows
goarch:
- amd64
- arm64
ldflags:
# The v prefix is stripped by goreleaser, so we need to add it back.
# https://goreleaser.com/customization/templates/#fnref:version-prefix
- "-s -w -X main.version=v{{ .Version }}"
archives:
- format: binary
name_template: >-
{{ .ProjectName }}_{{- tolower .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
use: github-native
tparse-0.18.0/CHANGELOG.md 0000664 0000000 0000000 00000011264 15052652102 0014711 0 ustar 00root root 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [v0.18.0] - 2025-08-24
- Wrap panic messages at the terminal width (#142)
- Do not include packages with no coverage in the output (#144)
## [v0.17.0]
- Deprecate github.com/mfridman/buildversion, and use std lib `debug.ReadBuildInfo()` instead. In
go1.24 this is handled automatically, from the [release notes](https://go.dev/doc/go1.24):
> The go build command now sets the main module’s version in the compiled binary based on the
> version control system tag and/or commit. A +dirty suffix will be appended if there are
> uncommitted changes. Use the -buildvcs=false flag to omit version control information from the
> binary.
- Handle changes in go1.24 related to build output. `tparse` will pipe the build output to stderr
> Furthermore, `go test -json` now reports build output and failures in JSON, interleaved with
> test result JSON. These are distinguished by new Action types, but if they cause problems in a
> test integration system, you can revert to the text build output with GODEBUG setting
> gotestjsonbuildtext=1.
## [v0.16.0]
- Add a `-follow-output` flag to allow writing go test output directly into a file. This will be
useful (especially in CI jobs) for outputting overly verbose testing output into a file instead of
the standard stream. (#134)
| flag combination | `go test` output destination |
| ------------------------ | ---------------------------- |
| No flags | Discard output |
| `-follow` | Write to stdout |
| `-follow-output` | Write to file |
| `-follow -follow-output` | Write to file |
- Use [charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) for table rendering.
- This will allow for more control over the output and potentially more features in the future.
(#136)
- Minor changes to the output format are expected, but the overall content should remain the same.
If you have any feedback, please let me know.
## [v0.15.0]
- Add `-trimpath` flag, which removes the path prefix from package names in the output, simplifying
their display. See #128 for examples.
- There's a special case for `-trimpath=auto` which will automatically determine the prefix based
on the longest common prefix of all package paths.
## [v0.14.0]
- Modify `--follow` behavior by minimizing noisy output. (#122)
> [!TIP]
>
> If you want the existing behavior, I added a `--follow-verbose` flag. But please do let me know if
> this affected you, as I plan to remove this before cutting a `v1.0.0`. Thank you!
## [v0.13.3]
- General housekeeping and dependency updates.
## [v0.13.2]
- Add partial support for `-compare`. A feature that displays the coverage difference against a
previous run. See description for more details
https://github.com/mfridman/tparse/pull/101#issue-1857786730 and the initial issue #92.
- Fix unstable common package prefix logic #104
## [v0.13.1] - 2023-08-04
- Fix failing GoReleaser GitHub action (release notes location).
Summary from [v0.13.0](https://github.com/mfridman/tparse/releases/tag/v0.13.0)
- Start a [CHANGELOG.md](https://github.com/mfridman/tparse/blob/main/CHANGELOG.md) for user-facing
change.
- Add [GoReleaser](https://goreleaser.com/) to automate the release process. Pre-built binaries are
available for each release, currently Linux and macOS. If there is demand, can also add Windows.
## [v0.13.0] - 2023-08-04
- Start a [CHANGELOG.md](https://github.com/mfridman/tparse/blob/main/CHANGELOG.md) for user-facing
change.
- Add [GoReleaser](https://goreleaser.com/) to automate the release process. Pre-built binaries are
available for each release, currently Linux and macOS. If there is demand, can also add Windows.
[Unreleased]: https://github.com/mfridman/tparse/compare/v0.18.0...HEAD
[v0.18.0]: https://github.com/mfridman/tparse/compare/v0.17.0...v0.18.0
[v0.17.0]: https://github.com/mfridman/tparse/compare/v0.16.0...v0.17.0
[v0.16.0]: https://github.com/mfridman/tparse/compare/v0.15.0...v0.16.0
[v0.15.0]: https://github.com/mfridman/tparse/compare/v0.14.0...v0.15.0
[v0.14.0]: https://github.com/mfridman/tparse/compare/v0.13.3...v0.14.0
[v0.13.3]: https://github.com/mfridman/tparse/compare/v0.13.2...v0.13.3
[v0.13.2]: https://github.com/mfridman/tparse/compare/v0.13.1...v0.13.2
[v0.13.1]: https://github.com/mfridman/tparse/compare/v0.13.0...v0.13.1
[v0.13.0]: https://github.com/mfridman/tparse/releases/tag/v0.13.0
tparse-0.18.0/LICENSE 0000664 0000000 0000000 00000002060 15052652102 0014077 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2018 Michael Fridman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
tparse-0.18.0/Makefile 0000664 0000000 0000000 00000003266 15052652102 0014543 0 ustar 00root root 0000000 0000000 ROOT := github.com/mfridman/tparse
GOPATH ?= $(shell go env GOPATH)
TOOLS_BIN = $(GOPATH)/bin
.PHONY: vet
vet:
@go vet ./...
.PHONY: lint
lint: tools
@golangci-lint run ./... --fix
.PHONY: tools
tools:
@which golangci-lint >/dev/null 2>&1 || \
(echo "Installing latest golangci-lint" && \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b "$(TOOLS_BIN)")
.PHONY: tools-update
tools-update:
@echo "Updating golangci-lint to latest version"
@rm -f "$(TOOLS_BIN)/golangci-lint"
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b "$(TOOLS_BIN)"
@echo "golangci-lint updated successfully to latest version"
.PHONY: tools-version
tools-version:
@echo "Current tool versions:"
@echo "golangci-lint: $$(golangci-lint --version 2>/dev/null || echo 'not installed')"
.PHONY: release
release:
@goreleaser --rm-dist
.PHONY: build
build:
@go build -o $$GOBIN/tparse ./
.PHONY: clean
clean:
@find . -type f -name '*.FAIL' -delete
.PHONY: test
test:
@go test -count=1 ./...
test-tparse:
@go test -race -count=1 ./internal/... -json -cover | go run main.go -trimpath=auto -sort=elapsed
@go test -race -count=1 ./tests/... -json -cover -coverpkg=./parse | go run main.go -trimpath=github.com/mfridman/tparse/ -sort=elapsed
# dogfooding :)
test-tparse-full:
go test -race -count=1 -v ./... -json | go run main.go -all -smallscreen -notests -sort=elapsed
coverage:
go test ./tests/... -coverpkg=./parse -covermode=count -coverprofile=count.out
go tool cover -html=count.out
search-todo:
@echo "Searching for TODOs in Go files..."
@rg '// TODO\(mf\):' --glob '*.go' || echo "No TODOs found."
tparse-0.18.0/README.md 0000664 0000000 0000000 00000005211 15052652102 0014352 0 ustar 00root root 0000000 0000000 # tparse [](https://github.com/mfridman/tparse)
A command line tool for analyzing and summarizing `go test` output.
> [!TIP]
>
> Don't forget to run `go test` with the `-json` flag.
Pass | Fail
:-------------------------:|:-------------------------:
|
By default, `tparse` will always return test failures and panics, if any, followed by a package-level summary table.
To get additional info on passed tests run `tparse` with `-pass` flag. Tests are grouped by package and sorted by elapsed time in descending order (longest to shortest).
### [But why?!](#but-why) for more info.
## Installation
go install github.com/mfridman/tparse@latest
Or download the latest pre-built binary [here](https://github.com/mfridman/tparse/releases/latest).
## Usage
Once `tparse` is installed there are 2 ways to use it:
1. Run `go test` as normal, but add `-json` flag and pipe output to `tparse`.
```
set -o pipefail && go test fmt -json | tparse -all
```
2. Save the output of `go test` with `-json` flag into a file and call `tparse` with `-file` option.
```
go test fmt -json > fmt.out
tparse -all -file=fmt.out
```
Tip: run `tparse -h` to get usage and options.
## But why?!
`go test` is awesome, but verbose. Sometimes you just want readily available failures, grouped by package, printed with a dash of color.
`tparse` attempts to do just that; return failed tests and panics, if any, followed by a single package-level summary. No more searching for the literal string: "--- FAIL".
But, let's take it a bit further. With `-all` (`-pass` and `-skip` combined) you can get additional info, such as skipped tests and elapsed time of each passed test.
`tparse` comes with a `-follow` flag to print raw output. Yep, go test pipes JSON, it's parsed and the output is printed back out as if you ran go test without `-json` flag. Eliminating the need for `tee /dev/tty` between pipes.
The default print order is:
- `go test` output (if adding `-follow` flag)
- passed/skipped table (if adding `-all`, `-skip` or `-pass` flag)
- failed tests and panics
- summary
For narrow displays the `-smallscreen` flag may be useful, dividing a long test name and making it vertical heavy:
```
TestSubtests/an_awesome_but_long/subtest_for_the/win
TestSubtests
/an_awesome_but_long
/subtest_for_the
/win
```
`tparse` aims to be a simple alternative to one-liner bash functions.
tparse-0.18.0/go.mod 0000664 0000000 0000000 00000001661 15052652102 0014206 0 ustar 00root root 0000000 0000000 module github.com/mfridman/tparse
go 1.23.0
toolchain go1.24.2
require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/muesli/termenv v0.16.0
github.com/stretchr/testify v1.9.0
golang.org/x/term v0.32.0
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.9.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
tparse-0.18.0/go.sum 0000664 0000000 0000000 00000010152 15052652102 0014226 0 ustar 00root root 0000000 0000000 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY=
github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tparse-0.18.0/internal/ 0000775 0000000 0000000 00000000000 15052652102 0014710 5 ustar 00root root 0000000 0000000 tparse-0.18.0/internal/app/ 0000775 0000000 0000000 00000000000 15052652102 0015470 5 ustar 00root root 0000000 0000000 tparse-0.18.0/internal/app/app.go 0000664 0000000 0000000 00000010065 15052652102 0016601 0 ustar 00root root 0000000 0000000 package app
import (
"errors"
"fmt"
"io"
"os"
"github.com/mfridman/tparse/parse"
)
type Options struct {
// Output is used to write the final output, such as the tables, summary, etc.
Output io.Writer
// DisableColor will disable all colors.
DisableColor bool
// Format will set the output format for tables.
Format OutputFormat
// Sorter will set the sort order for the table.
Sorter parse.PackageSorter
// ShowNoTests will display packages containing no test files or empty test files.
ShowNoTests bool
// FileName will read test output from a file.
FileName string
// Test table options
TestTableOptions TestTableOptions
SummaryTableOptions SummaryTableOptions
// FollowOutput will follow the raw output as go test is running.
FollowOutput bool // Output to stdout
FollowOutputWriter io.WriteCloser // Output to a file, takes precedence over FollowOutput
FollowOutputVerbose bool
// Progress will print a single summary line for each package once the package has completed.
// Useful for long running test suites. Maybe used with FollowOutput or on its own.
//
// This will output to stdout.
Progress bool
ProgressOutput io.Writer
// DisableTableOutput will disable all table output. This is used for testing.
DisableTableOutput bool
//
// Experimental
//
// Compare includes a diff of a previous test output file in the summary table.
Compare string
}
func Run(option Options) (int, error) {
var reader io.ReadCloser
var err error
if option.FileName != "" {
if reader, err = os.Open(option.FileName); err != nil {
return 1, err
}
} else {
if reader, err = newPipeReader(); err != nil {
return 1, errors.New("stdin must be a pipe, or use -file to open a go test output file")
}
}
defer reader.Close()
if option.FollowOutputWriter != nil {
defer option.FollowOutputWriter.Close()
}
summary, err := parse.Process(
reader,
parse.WithFollowOutput(option.FollowOutput),
parse.WithFollowVersboseOutput(option.FollowOutputVerbose),
parse.WithWriter(option.FollowOutputWriter),
parse.WithProgress(option.Progress),
parse.WithProgressOutput(option.ProgressOutput),
)
if err != nil {
return 1, err
}
if len(summary.Packages) == 0 {
return 1, fmt.Errorf("found no go test packages")
}
// Useful for tests that don't need tparse table output. Very useful for testing output from
// [parse.Process]
if !option.DisableTableOutput {
display(option.Output, summary, option)
}
return summary.ExitCode(), nil
}
func newPipeReader() (io.ReadCloser, error) {
finfo, err := os.Stdin.Stat()
if err != nil {
return nil, err
}
// Check file mode bits to test for named pipe as stdin.
if finfo.Mode()&os.ModeNamedPipe != 0 {
return os.Stdin, nil
}
return nil, errors.New("stdin must be a pipe")
}
func display(w io.Writer, summary *parse.GoTestSummary, option Options) {
// Best effort to open the compare against file, if it exists.
var warnings []string
defer func() {
for _, w := range warnings {
fmt.Fprintf(os.Stderr, "warning: %s\n", w)
}
}()
var against *parse.GoTestSummary
if option.Compare != "" {
// TODO(mf): cleanup, this is messy.
f, err := os.Open(option.Compare)
if err != nil {
warnings = append(warnings, fmt.Sprintf("failed to open against file: %s", option.Compare))
} else {
defer f.Close()
against, err = parse.Process(f)
if err != nil {
warnings = append(warnings, fmt.Sprintf("failed to parse against file: %s", option.Compare))
}
}
}
cw := newConsoleWriter(w, option.Format, option.DisableColor)
// Sort packages by name ASC.
packages := summary.GetSortedPackages(option.Sorter)
// Only print the tests table if either pass or skip is true.
if option.TestTableOptions.Pass || option.TestTableOptions.Skip {
if option.Format == OutputFormatMarkdown {
cw.testsTableMarkdown(packages, option.TestTableOptions)
} else {
cw.testsTable(packages, option.TestTableOptions)
}
}
// Failures (if any) and summary table are always printed.
cw.printFailed(packages)
cw.summaryTable(packages, option.ShowNoTests, option.SummaryTableOptions, against)
}
tparse-0.18.0/internal/app/console_writer.go 0000664 0000000 0000000 00000004137 15052652102 0021062 0 ustar 00root root 0000000 0000000 package app
import (
"io"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
type OutputFormat int
const (
// OutputFormatBasic is a normal table without a border
OutputFormatPlain OutputFormat = iota + 1
// OutputFormatBasic is a normal table with border
OutputFormatBasic
// OutputFormatBasic is a markdown-rendered table
OutputFormatMarkdown
)
type consoleWriter struct {
format OutputFormat
w io.Writer
red colorOptionFunc
green colorOptionFunc
yellow colorOptionFunc
}
type colorOptionFunc func(s string) string
// newColor is a helper function to set the base color.
func newColor(color lipgloss.TerminalColor) colorOptionFunc {
return func(text string) string {
return lipgloss.NewStyle().Foreground(color).Render(text)
}
}
// newMarkdownColor is a helper function to set the base color for markdown.
func newMarkdownColor(s string) colorOptionFunc {
return func(text string) string {
return s + " " + text
}
}
func noColor() colorOptionFunc {
return func(text string) string { return text }
}
func newConsoleWriter(w io.Writer, format OutputFormat, disableColor bool) *consoleWriter {
if format == 0 {
format = OutputFormatBasic
}
cw := &consoleWriter{
w: w,
format: format,
}
cw.red = noColor()
cw.green = noColor()
cw.yellow = noColor()
if !disableColor {
// NOTE(mf): GitHub Actions CI env (and probably others) do not have an
// interactive TTY, and tparse through termenv will degrade to the
// "best available option" .. which is no colors. We can work around this by
// setting a color profile explicitly instead of relying on termenv to auto-detect.
// Ref: https://github.com/charmbracelet/lipgloss/issues/74
// Ref: https://github.com/mfridman/tparse/issues/76
lipgloss.SetColorProfile(termenv.TrueColor)
switch format {
case OutputFormatMarkdown:
cw.green = newMarkdownColor("🟢")
cw.yellow = newMarkdownColor("🟡")
cw.red = newMarkdownColor("🔴")
default:
cw.green = newColor(lipgloss.Color("10"))
cw.yellow = newColor(lipgloss.Color("11"))
cw.red = newColor(lipgloss.Color("9"))
}
}
return cw
}
tparse-0.18.0/internal/app/table.go 0000664 0000000 0000000 00000002230 15052652102 0017103 0 ustar 00root root 0000000 0000000 package app
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
func newTable(
format OutputFormat,
override func(style lipgloss.Style, row, col int) lipgloss.Style,
) *table.Table {
tbl := table.New()
switch format {
case OutputFormatPlain:
tbl.Border(lipgloss.HiddenBorder()).BorderTop(false).BorderBottom(false)
case OutputFormatMarkdown:
tbl.Border(markdownBorder).BorderBottom(false).BorderTop(false)
case OutputFormatBasic:
tbl.Border(lipgloss.RoundedBorder())
}
return tbl.StyleFunc(func(row, col int) lipgloss.Style {
// Default style, may be overridden.
style := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Align(lipgloss.Center)
if override != nil {
style = override(style, row, col)
}
return style
})
}
var markdownBorder = lipgloss.Border{
Top: "-",
Bottom: "-",
Left: "|",
Right: "|",
TopLeft: "", // empty for markdown
TopRight: "", // empty for markdown
BottomLeft: "", // empty for markdown
BottomRight: "", // empty for markdown
MiddleLeft: "|",
MiddleRight: "|",
Middle: "|",
MiddleTop: "|",
MiddleBottom: "|",
}
tparse-0.18.0/internal/app/table_failed.go 0000664 0000000 0000000 00000014110 15052652102 0020407 0 ustar 00root root 0000000 0000000 package app
import (
"fmt"
"os"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
"github.com/mfridman/tparse/parse"
)
const (
defaultWidth = 96
)
// printFailed prints all failed tests, grouping them by package. Packages are sorted.
// Panic is an exception.
func (c *consoleWriter) printFailed(packages []*parse.Package) {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
width = defaultWidth
}
for _, pkg := range packages {
if pkg.HasPanic {
// TODO(mf): document why panics are handled separately. A panic may or may
// not be associated with tests, so we print it at the package level.
output := c.prepareStyledPanic(pkg.Summary.Package, pkg.Summary.Test, pkg.PanicEvents, width)
fmt.Fprintln(c.w, output)
continue
}
failedTests := pkg.TestsByAction(parse.ActionFail)
if len(failedTests) == 0 {
continue
}
styledPackageHeader := c.styledHeader(
pkg.Summary.Action.String(),
pkg.Summary.Package,
)
fmt.Fprintln(c.w, styledPackageHeader)
fmt.Fprintln(c.w)
/*
Failed tests are all the individual tests, where the subtests are not separated.
We need to sort the tests by name to ensure they are grouped together
*/
sort.Slice(failedTests, func(i, j int) bool {
return failedTests[i].Name < failedTests[j].Name
})
divider := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderTop(true).
Faint(c.format != OutputFormatMarkdown).
Width(width)
/*
Note, some output such as the "--- FAIL: " line is prefixed
with spaces. Unfortunately when dumping this in markdown format
it renders as an code block.
"To produce a code block in Markdown, simply indent every line of the
block by at least 4 spaces or 1 tab."
Ref. https://daringfireball.net/projects/markdown/syntax
Example:
--- FAIL: Test (0.05s)
--- FAIL: Test/test_01 (0.01s)
--- FAIL: Test/test_01/sort (0.00s)
This is why we wrap the entire test output in a code block.
*/
if c.format == OutputFormatMarkdown {
fmt.Fprintln(c.w, fencedCodeBlock)
}
var key string
for i, t := range failedTests {
// Add top divider to all tests except first one.
base, _, _ := cut(t.Name, "/")
if i > 0 && key != base {
fmt.Fprintln(c.w, divider.String())
}
key = base
fmt.Fprintln(c.w, c.prepareStyledTest(t))
}
if c.format == OutputFormatMarkdown {
fmt.Fprint(c.w, fencedCodeBlock+"\n\n")
}
}
}
const (
fencedCodeBlock string = "```"
)
// copied directly from strings.Cut (go1.18) to support older Go versions.
// In the future, replace this with the upstream function.
func cut(s, sep string) (before, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
func (c *consoleWriter) prepareStyledPanic(
packageName string,
testName string,
panicEvents []*parse.Event,
width int,
) string {
if testName != "" {
packageName = packageName + " • " + testName
}
styledPackageHeader := c.styledHeader("PANIC", packageName)
// TODO(mf): can we pass this panic stack to another package and either by default,
// or optionally, build human-readable panic output with:
// https://github.com/maruel/panicparse
var rows strings.Builder
for _, e := range panicEvents {
if e.Output == "" {
continue
}
rows.WriteString(e.Output)
}
content := lipgloss.NewStyle().Width(width).Render(rows.String())
return lipgloss.JoinVertical(lipgloss.Left, styledPackageHeader, content)
}
func (c *consoleWriter) styledHeader(status, packageName string) string {
status = c.red(strings.ToUpper(status))
packageName = strings.TrimSpace(packageName)
if c.format == OutputFormatMarkdown {
msg := fmt.Sprintf("## %s • %s", status, packageName)
return msg
// TODO(mf): an alternative implementation is to add 2 horizontal lines above and below
// the package header output.
//
// var divider string
// for i := 0; i < len(msg); i++ {
// divider += "─"
// }
// return fmt.Sprintf("%s\n%s\n%s", divider, msg, divider)
}
/*
Need to rethink how to best support multiple output formats across
CI, local terminal development and markdown
See https://github.com/mfridman/tparse/issues/71
*/
headerStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color("103"))
statusStyle := lipgloss.NewStyle().
PaddingLeft(3).
PaddingRight(2).
Foreground(lipgloss.Color("9"))
packageNameStyle := lipgloss.NewStyle().
PaddingRight(3)
headerRow := lipgloss.JoinHorizontal(
lipgloss.Left,
statusStyle.Render(status),
packageNameStyle.Render("package: "+packageName),
)
return headerStyle.Render(headerRow)
}
const (
failLine = "--- FAIL: "
)
func (c *consoleWriter) prepareStyledTest(t *parse.Test) string {
t.SortEvents()
var rows, headerRows strings.Builder
for _, e := range t.Events {
// Only add events that have output information. Skip everything else.
// Note, since we know about all the output, we can bubble "--- Fail" to the top
// of the output so it's trivial to spot the failing test name and elapsed time.
if e.Action != parse.ActionOutput {
continue
}
if strings.Contains(e.Output, failLine) {
header := strings.TrimSuffix(e.Output, "\n")
// go test prefixes too much padding to the "--- FAIL: " output lines.
// Let's cut the padding by half, being careful to preserve the fail
// line and the proceeding output.
before, after, ok := cut(header, failLine)
var pad string
if ok {
var n int
for _, r := range before {
if r == 32 {
n++
}
}
for i := 0; i < n/2; i++ {
pad += " "
}
}
header = pad + failLine + after
// Avoid colorizing markdown output so it renders properly, otherwise add a subtle
// red color to the test headers.
if c.format != OutputFormatMarkdown {
header = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render(header)
}
headerRows.WriteString(header)
continue
}
if e.Output != "" {
rows.WriteString(e.Output)
}
}
out := headerRows.String()
if rows.Len() > 0 {
out += "\n\n" + rows.String()
}
return out
}
tparse-0.18.0/internal/app/table_summary.go 0000664 0000000 0000000 00000016536 15052652102 0020676 0 ustar 00root root 0000000 0000000 package app
import (
"fmt"
"path"
"strconv"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/mfridman/tparse/internal/utils"
"github.com/mfridman/tparse/parse"
)
type SummaryTableOptions struct {
// For narrow screens, remove common prefix and trim long package names vertically. Example:
// github.com/mfridman/tparse/app
// github.com/mfridman/tparse/internal/seed-up-down-to-zero
//
// tparse/app
// tparse
// /seed-up-down-to-zero
Trim bool
// TrimPath is the path prefix to trim from the package name.
TrimPath string
}
func (c *consoleWriter) summaryTable(
packages []*parse.Package,
showNoTests bool,
options SummaryTableOptions,
against *parse.GoTestSummary,
) {
tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style {
switch row {
case table.HeaderRow:
default:
if col == 2 {
// Package name
style = style.Align(lipgloss.Left)
}
}
return style
})
header := summaryRow{
status: "Status",
elapsed: "Elapsed",
packageName: "Package",
cover: "Cover",
pass: "Pass",
fail: "Fail",
skip: "Skip",
}
tbl.Headers(header.toRow()...)
data := table.NewStringData()
// Capture as separate slices because notests are optional when passed tests are available.
// The only exception is if passed=0 and notests=1, then we display them regardless. This
// is almost always the user matching on the wrong package.
var passed, notests []summaryRow
names := make([]string, 0, len(packages))
for _, pkg := range packages {
names = append(names, pkg.Summary.Package)
}
packagePrefix := utils.FindLongestCommonPrefix(names)
for _, pkg := range packages {
elapsed := strconv.FormatFloat(pkg.Summary.Elapsed, 'f', 2, 64) + "s"
if pkg.Cached {
elapsed = "(cached)"
}
packageName := pkg.Summary.Package
packageName = shortenPackageName(packageName, packagePrefix, 32, options.Trim, options.TrimPath)
if pkg.HasPanic {
row := summaryRow{
status: c.red("PANIC"),
elapsed: elapsed,
packageName: packageName,
cover: "--", pass: "--", fail: "--", skip: "--",
}
data.Append(row.toRow())
continue
}
if pkg.HasFailedBuildOrSetup {
row := summaryRow{
status: c.red("FAIL"),
elapsed: elapsed,
packageName: packageName + "\n[" + pkg.Summary.Output + "]",
cover: "--", pass: "--", fail: "--", skip: "--",
}
data.Append(row.toRow())
continue
}
if pkg.NoTestFiles {
row := summaryRow{
status: c.yellow("NOTEST"),
elapsed: elapsed,
packageName: packageName + "\n[no test files]",
cover: "--", pass: "--", fail: "--", skip: "--",
}
notests = append(notests, row)
continue
}
if pkg.NoTests {
// This should capture cases where packages truly have no tests, but empty files.
if len(pkg.NoTestSlice) == 0 {
row := summaryRow{
status: c.yellow("NOTEST"),
elapsed: elapsed,
packageName: packageName + "\n[no tests to run]",
cover: "--", pass: "--", fail: "--", skip: "--",
}
notests = append(notests, row)
continue
}
// This should capture cases where packages have a mixture of empty and non-empty test files.
var ss []string
for i, t := range pkg.NoTestSlice {
i++
ss = append(ss, fmt.Sprintf("%d.%s", i, t.Test))
}
packageName := fmt.Sprintf("%s\n[no tests to run]\n%s", packageName, strings.Join(ss, "\n"))
row := summaryRow{
status: c.yellow("NOTEST"),
elapsed: elapsed,
packageName: packageName,
cover: "--", pass: "--", fail: "--", skip: "--",
}
notests = append(notests, row)
if len(pkg.TestsByAction(parse.ActionPass)) == len(pkg.NoTestSlice) {
continue
}
}
// TODO(mf): refactor this
// Separate cover colorization from the delta output.
coverage := "--"
if pkg.Cover {
coverage = fmt.Sprintf("%.1f%%", pkg.Coverage)
if against != nil {
againstP, ok := against.Packages[pkg.Summary.Package]
if ok {
var sign string
if pkg.Coverage > againstP.Coverage {
sign = "+"
}
coverage = fmt.Sprintf("%s (%s)", coverage, sign+strconv.FormatFloat(pkg.Coverage-againstP.Coverage, 'f', 1, 64)+"%")
} else {
coverage = fmt.Sprintf("%s (-)", coverage)
}
}
// Showing coverage for a package that failed is a bit odd.
//
// Only colorize the coverage when everything passed AND the output is not markdown.
if pkg.Summary.Action == parse.ActionPass && c.format != OutputFormatMarkdown {
switch cover := pkg.Coverage; {
case cover > 0.0 && cover <= 50.0:
coverage = c.red(coverage)
case pkg.Coverage > 50.0 && pkg.Coverage < 80.0:
coverage = c.yellow(coverage)
case pkg.Coverage >= 80.0:
coverage = c.green(coverage)
}
}
}
status := strings.ToUpper(pkg.Summary.Action.String())
switch pkg.Summary.Action {
case parse.ActionPass:
status = c.green(status)
case parse.ActionSkip:
status = c.yellow(status)
case parse.ActionFail:
status = c.red(status)
}
// Skip packages with no coverage to mimic nocoverageredesign behavior (changed in github.com/golang/go/issues/24570)
totalTests := len(pkg.TestsByAction(parse.ActionPass)) + len(pkg.TestsByAction(parse.ActionFail)) + len(pkg.TestsByAction(parse.ActionSkip))
if pkg.Cover && pkg.Coverage == 0.0 && totalTests == 0 {
continue
}
row := summaryRow{
status: status,
elapsed: elapsed,
packageName: packageName,
cover: coverage,
pass: strconv.Itoa(len(pkg.TestsByAction(parse.ActionPass))),
fail: strconv.Itoa(len(pkg.TestsByAction(parse.ActionFail))),
skip: strconv.Itoa(len(pkg.TestsByAction(parse.ActionSkip))),
}
passed = append(passed, row)
}
if data.Rows() == 0 && len(passed) == 0 && len(notests) == 0 {
return
}
for _, r := range passed {
data.Append(r.toRow())
}
// Only display the "no tests to run" cases if users want to see them when passed
// tests are available.
// An exception is made if there are no passed tests and only a single no test files
// package. This is almost always because the user forgot to match one or more packages.
if showNoTests || (len(passed) == 0 && len(notests) == 1) {
for _, r := range notests {
data.Append(r.toRow())
}
}
fmt.Fprintln(c.w, tbl.Data(data).Render())
}
type summaryRow struct {
status string
elapsed string
packageName string
cover string
pass string
fail string
skip string
}
func (r summaryRow) toRow() []string {
return []string{
r.status,
r.elapsed,
r.packageName,
r.cover,
r.pass,
r.fail,
r.skip,
}
}
func shortenPackageName(
name string,
prefix string,
maxLength int,
trim bool,
trimPath string,
) string {
if trimPath == "auto" {
name = strings.TrimPrefix(name, prefix)
} else if trimPath != "" {
name = strings.TrimPrefix(name, trimPath)
}
if !trim {
return name
}
if prefix == "" {
dir, name := path.Split(name)
// For SIV-style imports show the last non-versioned path identifier.
// Example: github.com/foo/bar/helper/v3 returns helper/v3
if dir != "" && versionMajorRe.MatchString(name) {
_, subpath := path.Split(path.Clean(dir))
name = path.Join(subpath, name)
}
return name
}
name = strings.TrimPrefix(name, prefix)
name = strings.TrimLeft(name, "/")
name = shortenTestName(name, true, maxLength)
return name
}
tparse-0.18.0/internal/app/table_tests.go 0000664 0000000 0000000 00000015214 15052652102 0020333 0 ustar 00root root 0000000 0000000 package app
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/mfridman/tparse/internal/utils"
"github.com/mfridman/tparse/parse"
)
var (
versionMajorRe = regexp.MustCompile(`(?m)v[0-9]+`)
)
type TestTableOptions struct {
// Display passed or skipped tests. If both are true this is equivalent to all.
Pass, Skip bool
// For narrow screens, trim long test identifiers vertically. Example:
// TestNoVersioning/seed-up-down-to-zero
//
// TestNoVersioning
// /seed-up-down-to-zero
Trim bool
// TrimPath is the path prefix to trim from the package name.
TrimPath string
// Display up to N slow tests for each package, tests are sorted by
// calculated the elapsed time for the given test.
Slow int
}
type packageTests struct {
skippedCount int
skipped []*parse.Test
passedCount int
passed []*parse.Test
}
func (c *consoleWriter) testsTable(packages []*parse.Package, option TestTableOptions) {
// Print passed tests, sorted by elapsed DESC. Grouped by alphabetically sorted packages.
tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style {
switch row {
case table.HeaderRow:
default:
if col == 2 || col == 3 {
// Test name and package name
style = style.Align(lipgloss.Left)
}
}
return style
})
header := testRow{
status: "Status",
elapsed: "Elapsed",
testName: "Test",
packageName: "Package",
}
tbl.Headers(header.toRow()...)
data := table.NewStringData()
names := make([]string, 0, len(packages))
for _, pkg := range packages {
names = append(names, pkg.Summary.Package)
}
packagePrefix := utils.FindLongestCommonPrefix(names)
for i, pkg := range packages {
// Discard packages where we cannot generate a sensible test summary.
if pkg.NoTestFiles || pkg.NoTests || pkg.HasPanic {
continue
}
pkgTests := getTestsFromPackages(pkg, option)
all := make([]*parse.Test, 0, len(pkgTests.passed)+len(pkgTests.skipped))
all = append(all, pkgTests.passed...)
all = append(all, pkgTests.skipped...)
for _, t := range all {
// TODO(mf): why are we sorting this?
t.SortEvents()
testName := shortenTestName(t.Name, option.Trim, 32)
status := strings.ToUpper(t.Status().String())
switch t.Status() {
case parse.ActionPass:
status = c.green(status)
case parse.ActionSkip:
status = c.yellow(status)
case parse.ActionFail:
status = c.red(status)
}
packageName := shortenPackageName(t.Package, packagePrefix, 16, option.Trim, option.TrimPath)
row := testRow{
status: status,
elapsed: strconv.FormatFloat(t.Elapsed(), 'f', 2, 64),
testName: testName,
packageName: packageName,
}
data.Append(row.toRow())
}
if i != (len(packages) - 1) {
// Add a blank row between packages.
data.Append(testRow{}.toRow())
}
}
if data.Rows() > 0 {
fmt.Fprintln(c.w, tbl.Data(data).Render())
}
}
func (c *consoleWriter) testsTableMarkdown(packages []*parse.Package, option TestTableOptions) {
for _, pkg := range packages {
// Print passed tests, sorted by elapsed DESC. Grouped by alphabetically sorted packages.
tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style {
switch row {
case table.HeaderRow:
default:
if col == 2 {
// Test name
style = style.Align(lipgloss.Left)
}
}
return style
})
header := []string{
"Status",
"Elapsed",
"Test",
}
tbl.Headers(header...)
data := table.NewStringData()
// Discard packages where we cannot generate a sensible test summary.
if pkg.NoTestFiles || pkg.NoTests || pkg.HasPanic {
continue
}
pkgTests := getTestsFromPackages(pkg, option)
all := make([]*parse.Test, 0, len(pkgTests.passed)+len(pkgTests.skipped))
all = append(all, pkgTests.passed...)
all = append(all, pkgTests.skipped...)
for _, t := range all {
// TODO(mf): why are we sorting this?
t.SortEvents()
testName := shortenTestName(t.Name, option.Trim, 32)
status := strings.ToUpper(t.Status().String())
switch t.Status() {
case parse.ActionPass:
status = c.green(status)
case parse.ActionSkip:
status = c.yellow(status)
case parse.ActionFail:
status = c.red(status)
}
data.Append([]string{
status,
strconv.FormatFloat(t.Elapsed(), 'f', 2, 64),
testName,
})
}
if data.Rows() > 0 {
fmt.Fprintf(c.w, "## 📦 Package **`%s`**\n", pkg.Summary.Package)
fmt.Fprintln(c.w)
msg := fmt.Sprintf("Tests: ✓ %d passed | %d skipped\n",
pkgTests.passedCount,
pkgTests.skippedCount,
)
if option.Slow > 0 && option.Slow < pkgTests.passedCount {
msg += fmt.Sprintf("↓ Slowest %d passed tests shown (of %d)\n",
option.Slow,
pkgTests.passedCount,
)
}
fmt.Fprint(c.w, msg)
fmt.Fprintln(c.w)
fmt.Fprintln(c.w, "")
fmt.Fprintln(c.w)
fmt.Fprintln(c.w, "Click for test summary
")
fmt.Fprintln(c.w)
fmt.Fprintln(c.w, tbl.Data(data).Render())
fmt.Fprintln(c.w, " ")
fmt.Fprintln(c.w)
}
fmt.Fprintln(c.w)
}
}
func getTestsFromPackages(pkg *parse.Package, option TestTableOptions) *packageTests {
tests := &packageTests{}
skipped := pkg.TestsByAction(parse.ActionSkip)
tests.skippedCount = len(skipped)
passed := pkg.TestsByAction(parse.ActionPass)
tests.passedCount = len(passed)
if option.Skip {
tests.skipped = append(tests.skipped, skipped...)
}
if option.Pass {
tests.passed = append(tests.passed, passed...)
// Order passed tests within a package by elapsed time DESC (longest on top).
sort.Slice(tests.passed, func(i, j int) bool {
return tests.passed[i].Elapsed() > tests.passed[j].Elapsed()
})
// Optional, display only the slowest N tests by elapsed time.
if option.Slow > 0 && len(tests.passed) > option.Slow {
tests.passed = tests.passed[:option.Slow]
}
}
return tests
}
func shortenTestName(s string, trim bool, maxLength int) string {
var testName strings.Builder
testName.WriteString(s)
if trim && testName.Len() > maxLength && strings.Count(testName.String(), "/") > 0 {
testName.Reset()
ss := strings.Split(s, "/")
testName.WriteString(ss[0] + "\n")
for i, s := range ss[1:] {
testName.WriteString(" /")
for len(s) > maxLength {
testName.WriteString(s[:maxLength-2] + " …\n ")
s = s[maxLength-2:]
}
testName.WriteString(s)
if i != len(ss[1:])-1 {
testName.WriteString("\n")
}
}
}
return testName.String()
}
type testRow struct {
status string
elapsed string
testName string
packageName string
}
func (r testRow) toRow() []string {
return []string{
r.status,
r.elapsed,
r.testName,
r.packageName,
}
}
tparse-0.18.0/internal/utils/ 0000775 0000000 0000000 00000000000 15052652102 0016050 5 ustar 00root root 0000000 0000000 tparse-0.18.0/internal/utils/utils.go 0000664 0000000 0000000 00000002346 15052652102 0017544 0 ustar 00root root 0000000 0000000 package utils
import (
"io"
"sort"
"strings"
)
// FindLongestCommonPrefix finds the longest common path prefix of a set of paths. For example,
// given the following:
//
// github.com/owner/repo/cmd/foo
// github.com/owner/repo/cmd/bar
//
// The longest common prefix is: github.com/owner/repo/cmd/ (note the trailing slash is included).
func FindLongestCommonPrefix(paths []string) string {
if len(paths) < 2 {
return ""
}
// Sort the paths to optimize comparison.
sort.Strings(paths)
first, last := paths[0], paths[len(paths)-1]
if first == last {
return first
}
// Find the common prefix between the first and last sorted paths.
commonPrefixLength := 0
minLength := min(len(first), len(last))
for commonPrefixLength < minLength && first[commonPrefixLength] == last[commonPrefixLength] {
commonPrefixLength++
}
// Ensure the common prefix ends at a boundary.
commonPrefix := first[:commonPrefixLength]
if n := strings.LastIndex(commonPrefix, "/"); n != -1 {
return commonPrefix[:n+1]
}
return ""
}
// DiscardCloser is an io.Writer that implements io.Closer by doing nothing.
//
// https://github.com/golang/go/issues/22823
type WriteNopCloser struct {
io.Writer
}
func (WriteNopCloser) Close() error {
return nil
}
tparse-0.18.0/internal/utils/utils_test.go 0000664 0000000 0000000 00000003267 15052652102 0020606 0 ustar 00root root 0000000 0000000 package utils
import "testing"
func TestFindLongestCommonPrefix(t *testing.T) {
t.Parallel()
tests := []struct {
paths []string
want string
}{
{
paths: []string{},
want: "",
},
{
paths: []string{
"github.com/user/project/pkg",
},
want: "",
},
{
paths: []string{
"github.com/user/project/pkg",
"github.com/user/project/pkg",
"github.com/user/project/pkg",
},
want: "github.com/user/project/pkg",
},
{
paths: []string{
"github.com/user/project/pkg",
"github.com/user/project/cmd",
},
want: "github.com/user/project/",
},
{
paths: []string{
"github.com/user/project/pkg",
"bitbucket.org/user/project/cmd",
},
want: "",
},
{
paths: []string{
"github.com/user/project/pkg",
"github.com/user/project/cmd",
"github.com/user/project/cmd/subcmd",
"github.com/nonuser/project/cmd/subcmd",
},
want: "github.com/",
},
{
paths: []string{
"github.com/foo/bar/baz/qux",
"github.com/foo/bar/baz",
"github.com/foo/bar/baz/qux/quux",
"github.com/foo/bar/baz/qux/quux/corge",
"github.com/foo/bar/baz/foo",
"github.com/foo/bar/baz/foo/bar",
},
want: "github.com/foo/bar/",
},
{
paths: []string{
"/",
},
want: "",
},
{
paths: []string{
"/",
"/",
},
want: "/",
},
{
paths: []string{
"/abc",
"/abc",
},
want: "/abc",
},
{
paths: []string{
"foo/bar/foo",
"foo/foo/foo",
},
want: "foo/",
},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
actual := FindLongestCommonPrefix(tt.paths)
if actual != tt.want {
t.Errorf("want %s, got %s", tt.want, actual)
}
})
}
}
tparse-0.18.0/main.go 0000664 0000000 0000000 00000013070 15052652102 0014350 0 ustar 00root root 0000000 0000000 package main
import (
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"runtime/debug"
"github.com/mfridman/tparse/internal/app"
"github.com/mfridman/tparse/internal/utils"
"github.com/mfridman/tparse/parse"
)
// Flags.
var (
vPtr = flag.Bool("v", false, "")
versionPtr = flag.Bool("version", false, "")
hPtr = flag.Bool("h", false, "")
helpPtr = flag.Bool("help", false, "")
allPtr = flag.Bool("all", false, "")
passPtr = flag.Bool("pass", false, "")
skipPtr = flag.Bool("skip", false, "")
showNoTestsPtr = flag.Bool("notests", false, "")
smallScreenPtr = flag.Bool("smallscreen", false, "")
noColorPtr = flag.Bool("nocolor", false, "")
slowPtr = flag.Int("slow", 0, "")
fileNamePtr = flag.String("file", "", "")
formatPtr = flag.String("format", "", "")
followPtr = flag.Bool("follow", false, "")
followOutputPtr = flag.String("follow-output", "", "")
sortPtr = flag.String("sort", "name", "")
progressPtr = flag.Bool("progress", false, "")
comparePtr = flag.String("compare", "", "")
trimPathPtr = flag.String("trimpath", "", "")
// Undocumented flags
followVerbosePtr = flag.Bool("follow-verbose", false, "")
// Legacy flags
noBordersPtr = flag.Bool("noborders", false, "")
)
var usage = `Usage:
go test ./... -json | tparse [options...]
go test [packages...] -json | tparse [options...]
go test [packages...] -json > pkgs.out ; tparse [options...] -file pkgs.out
Options:
-h Show help.
-v Show version.
-all Display table event for pass and skip. (Failed items always displayed)
-pass Display table for passed tests.
-skip Display table for skipped tests.
-notests Display packages containing no test files or empty test files.
-smallscreen Split subtest names vertically to fit on smaller screens.
-slow Number of slowest tests to display. Default is 0, display all.
-sort Sort table output by attribute [name, elapsed, cover]. Default is name.
-nocolor Disable all colors. (NO_COLOR also supported)
-format The output format for tables [basic, plain, markdown]. Default is basic.
-file Read test output from a file.
-follow Follow raw output from go test to stdout.
-follow-output Write raw output from go test to a file (takes precedence over -follow).
-progress Print a single summary line for each package. Useful for long running test suites.
-compare Compare against a previous test output file. (experimental)
-trimpath Remove path prefix from package names in output, simplifying their display.
`
var version string
func main() {
log.SetFlags(0)
flag.Usage = func() {
fmt.Fprint(flag.CommandLine.Output(), usage)
}
flag.Parse()
if *vPtr || *versionPtr {
if info, ok := debug.ReadBuildInfo(); ok {
version = info.Main.Version
}
fmt.Fprintf(os.Stdout, "tparse version: %s\n", version)
return
}
if *hPtr || *helpPtr {
fmt.Print(usage)
return
}
var format app.OutputFormat
switch *formatPtr {
case "basic":
format = app.OutputFormatBasic
case "plain":
format = app.OutputFormatPlain
case "markdown":
format = app.OutputFormatMarkdown
case "":
// This was an existing flag, let's try to avoid breaking users.
format = app.OutputFormatBasic
if *noBordersPtr {
format = app.OutputFormatPlain
}
default:
fmt.Fprintf(os.Stderr, "invalid option:%q. The -format flag must be one of: basic, plain or markdown\n", *formatPtr)
return
}
var sorter parse.PackageSorter
switch *sortPtr {
case "name":
sorter = parse.SortByPackageName
case "elapsed":
sorter = parse.SortByElapsed
case "cover":
sorter = parse.SortByCoverage
default:
fmt.Fprintf(os.Stderr, "invalid option:%q. The -sort flag must be one of: name, elapsed or cover\n", *sortPtr)
return
}
if *allPtr {
*passPtr = true
*skipPtr = true
}
// Show colors by default.
var disableColor bool
if _, ok := os.LookupEnv("NO_COLOR"); ok || *noColorPtr {
disableColor = true
}
var followOutput io.WriteCloser
switch {
case *followOutputPtr != "":
var err error
followOutput, err = os.Create(*followOutputPtr)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
*followPtr = true
case *followPtr, *followVerbosePtr:
followOutput = os.Stdout
default:
// If no follow flags are set, we should not write to followOutput.
followOutput = utils.WriteNopCloser{Writer: io.Discard}
}
// TODO(mf): we should marry the options with the flags to avoid having to do this.
options := app.Options{
Output: os.Stdout,
DisableColor: disableColor,
FollowOutput: *followPtr,
FollowOutputWriter: followOutput,
FollowOutputVerbose: *followVerbosePtr,
FileName: *fileNamePtr,
TestTableOptions: app.TestTableOptions{
Pass: *passPtr,
Skip: *skipPtr,
Trim: *smallScreenPtr,
TrimPath: *trimPathPtr,
Slow: *slowPtr,
},
SummaryTableOptions: app.SummaryTableOptions{
Trim: *smallScreenPtr,
TrimPath: *trimPathPtr,
},
Format: format,
Sorter: sorter,
ShowNoTests: *showNoTestsPtr,
Progress: *progressPtr,
ProgressOutput: os.Stdout,
Compare: *comparePtr,
// Do not expose publicly.
DisableTableOutput: false,
}
exitCode, err := app.Run(options)
if err != nil {
msg := err.Error()
if errors.Is(err, parse.ErrNotParsable) {
msg = "no parsable events: Make sure to run go test with -json flag"
}
fmt.Fprintln(os.Stderr, msg)
}
os.Exit(exitCode)
}
tparse-0.18.0/parse/ 0000775 0000000 0000000 00000000000 15052652102 0014206 5 ustar 00root root 0000000 0000000 tparse-0.18.0/parse/event.go 0000664 0000000 0000000 00000020634 15052652102 0015663 0 ustar 00root root 0000000 0000000 package parse
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
var (
coverRe = regexp.MustCompile(`[0-9]{1,3}\.[0-9]{1}\%`)
failedBuildOrSetupRe = regexp.MustCompile(`^FAIL(.*)\[(build failed|setup failed)\]`)
)
// Event represents a single line of JSON output from go test with the -json flag.
//
// For more info see, https://golang.org/cmd/test2json and
// https://github.com/golang/go/blob/master/src/cmd/internal/test2json/test2json.go
type Event struct {
// Action can be one of: run, pause, cont, pass, bench, fail, output, skip
//
// Added in go1.20:
// - start
//
// Added in go1.24:
// - build-fail
// - build-output
Action Action
// Portion of the test's output (standard output and standard error merged together)
Output string
// Time at which the event occurred, encodes as an RFC3339-format string.
// It is conventionally omitted for cached test results.
Time time.Time
// The Package field, if present, specifies the package being tested.
// When the go command runs parallel tests in -json mode, events from
// different tests are interlaced; the Package field allows readers to separate them.
Package string
// The Test field, if present, specifies the test, example, or benchmark
// function that caused the event. Events for the overall package test do not set Test.
Test string
// Elapsed is time elapsed (in seconds) for the specific test or
// the overall package test that passed or failed.
Elapsed float64
// FailedBuild is the package ID that is the root cause of a build failure for this test. This
// will be reported in the final "fail" event's FailedBuild field.
FailedBuild string
// BuildEvent specific fields.
//
// TODO(mf): Unfortunately the output has both BuildEvent and TestEvent interleaved in the
// output so for now we just combine them. But in the future pre-v1 we'll want to improve this.
//
// type BuildEvent struct {
// ImportPath string
// Action string
// Output string
// }
ImportPath string
}
func (e *Event) String() string {
return fmt.Sprintf(
"%-6s - %s - %s elapsed[%.2f] - time[%s]\n%v",
strings.ToUpper(e.Action.String()),
e.Package,
e.Test,
e.Elapsed,
e.Time.Format(time.StampMicro),
e.Output,
)
}
// NewEvent attempts to decode data into an Event.
func NewEvent(data []byte) (*Event, error) {
var e Event
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return &e, nil
}
// DiscardOutput reports whether to discard output that belongs to one of
// the output update actions:
// === RUN
// === PAUSE
// === CONT
// If output is none one of the above return false.
func (e *Event) DiscardOutput() bool {
for i := range updates {
if strings.HasPrefix(e.Output, updates[i]) {
return true
}
}
return false
}
func (e *Event) DiscardEmptyTestOutput() bool {
return e.Action == ActionOutput && e.Test == ""
}
// Prefixes for the different types of test updates. See:
//
// https://github.com/golang/go/blob/38cfb3be9d486833456276777155980d1ec0823e/src/cmd/internal/test2json/test2json.go#L160-L168
const (
updatePrefixRun = "=== RUN "
updatePrefixPause = "=== PAUSE "
updatePrefixCont = "=== CONT "
updatePrefixName = "=== NAME "
updatePrefixPass = "=== PASS "
updatePrefixFail = "=== FAIL "
updatePrefixSkip = "=== SKIP "
)
var updates = []string{
updatePrefixRun,
updatePrefixPause,
updatePrefixCont,
}
// Prefix for the different types of test results. See
//
// https://github.com/golang/go/blob/38cfb3be9d486833456276777155980d1ec0823e/src/cmd/internal/test2json/test2json.go#L170-L175
const (
resultPrefixPass = "--- PASS: "
resultPrefixFail = "--- FAIL: "
resultPrefixSkip = "--- SKIP: "
resultPrefixBench = "--- BENCH: "
)
// BigResult reports whether the test output is a big pass or big fail
//
// https://github.com/golang/go/blob/38cfb3be9d486833456276777155980d1ec0823e/src/cmd/internal/test2json/test2json.go#L146-L150
const (
bigPass = "PASS"
bigFail = "FAIL"
)
// Let's try using the LastLine method to report the package result.
// If there are issues with LastLine() we can switch to this method.
//
// BigResult reports whether the package passed or failed.
// func (e *Event) BigResult() bool {
// return e.Test == "" && (e.Output == "PASS\n" || e.Output == "FAIL\n")
// }
// LastLine reports whether the event is the final emitted output line summarizing the package run.
//
// ok github.com/astromail/rover/tests 0.583s
// {Time:2018-10-14 11:45:03.489687 -0400 EDT Action:pass Output: Package:github.com/astromail/rover/tests Test: Elapsed:0.584}
//
// FAIL github.com/astromail/rover/tests 0.534s
// {Time:2018-10-14 11:45:23.916729 -0400 EDT Action:fail Output: Package:github.com/astromail/rover/tests Test: Elapsed:0.53}
func (e *Event) LastLine() bool {
return e.Test == "" && e.Output == "" && (e.Action == ActionPass || e.Action == ActionFail)
}
// NoTestFiles reports special event case for packages containing no test files:
// "? \tpackage\t[no test files]\n"
func (e *Event) NoTestFiles() bool {
return strings.HasPrefix(e.Output, "? \t") && strings.HasSuffix(e.Output, "[no test files]\n")
}
// NoTestsToRun reports special event case for no tests to run:
// "ok \tgithub.com/some/awesome/module\t4.543s [no tests to run]\n"
func (e *Event) NoTestsToRun() bool {
return strings.HasPrefix(e.Output, "ok \t") && strings.HasSuffix(e.Output, "[no tests to run]\n")
}
// NoTestsWarn whether the event is a test that identifies as: "testing: warning: no tests to run\n"
//
// NOTE: can be found in a package or test event. Must check for non-empty test name in the event.
func (e *Event) NoTestsWarn() bool {
return e.Test != "" && e.Output == "testing: warning: no tests to run\n"
}
// IsCached reports special event case for cached packages:
// "ok \tgithub.com/mfridman/tparse/tests\t(cached)\n"
// "ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n"
func (e *Event) IsCached() bool {
return strings.HasPrefix(e.Output, "ok \t") && strings.Contains(e.Output, "\t(cached)")
}
// Cover reports special event case for package coverage:
// "ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n"
// "ok \tgithub.com/mfridman/srfax\t0.027s\tcoverage: 28.8% of statements\n"
// "ok \tgithub.com/mfridman/tparse/tests\t0.516s\tcoverage: 34.5% of statements in ./...\n"
func (e *Event) Cover() (float64, bool) {
var f float64
var err error
if strings.Contains(e.Output, "coverage:") && strings.Contains(e.Output, "of statements") {
s := coverRe.FindString(e.Output)
f, err = strconv.ParseFloat(strings.TrimRight(s, "%"), 64)
if err != nil {
return f, false
}
return f, true
}
return f, false
}
// IsRace indicates a race event has been detected.
func (e *Event) IsRace() bool {
return strings.HasPrefix(e.Output, "WARNING: DATA RACE")
}
// IsPanic indicates a panic event has been detected.
func (e *Event) IsPanic() bool {
// Let's see how this goes. If a user has this in one of their output lines, I think it's
// defensible to suggest updating their output.
if strings.HasPrefix(e.Output, "panic: ") {
return true
}
// The golang/go test suite occasionally outputs these keywords along with "as expected":
// time_test.go:1359: panic in goroutine 7, as expected, with "runtime error: racy use of timers"
if strings.Contains(e.Output, "runtime error:") && !strings.Contains(e.Output, "as expected") {
return true
}
return false
}
// Action is one of a fixed set of actions describing a single emitted event.
type Action string
// Prefixed with Action for convenience.
const (
ActionRun Action = "run" // test has started running
ActionPause Action = "pause" // test has been paused
ActionCont Action = "cont" // the test has continued running
ActionPass Action = "pass" // test passed
ActionBench Action = "bench" // benchmark printed log output but did not fail
ActionFail Action = "fail" // test or benchmark failed
ActionOutput Action = "output" // test printed output
ActionSkip Action = "skip" // test was skipped or the package contained no tests
// Added in go1.20 to denote the beginning of each test program's execution.
ActionStart Action = "start" // the start at the beginning of each test program's execution
// Added in go1.24 to denote a build failure.
ActionBuildFail Action = "build-fail" // the build failed
ActionBuildOutput Action = "build-output" // the toolchain printed output
)
func (a Action) String() string {
return string(a)
}
tparse-0.18.0/parse/event_test.go 0000664 0000000 0000000 00000031453 15052652102 0016723 0 ustar 00root root 0000000 0000000 package parse
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewEvent(t *testing.T) {
t.Parallel()
tt := []struct {
raw string
action Action
pkg string
test string
output string
discardOutput bool
lastLine bool
discardEmptyTestOutput bool
}{
{
// 0
`{"Time":"2018-10-15T21:03:52.728302-04:00","Action":"run","Package":"fmt","Test":"TestFmtInterface"}`,
ActionRun, "fmt", "TestFmtInterface", "", false, false, false,
},
{
// 1
`{"Time":"2018-10-15T21:03:56.232164-04:00","Action":"output","Package":"strings","Test":"ExampleBuilder","Output":"--- PASS: ExampleBuilder (0.00s)\n"}`,
ActionOutput, "strings", "ExampleBuilder", "--- PASS: ExampleBuilder (0.00s)\n", false, false, false,
},
{
// 2
`{"Time":"2018-10-15T21:03:56.235807-04:00","Action":"pass","Package":"strings","Elapsed":3.5300000000000002}`,
ActionPass, "strings", "", "", false, true, false,
},
{
// 3
`{"Time":"2018-10-15T21:00:51.379156-04:00","Action":"pass","Package":"fmt","Elapsed":0.066}`,
ActionPass, "fmt", "", "", false, true, false,
},
{
// 4
`{"Time":"2018-10-15T22:57:28.23799-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Elapsed":0.582}`,
ActionPass, "github.com/astromail/rover/tests", "", "", false, true, false,
},
{
// 5
`{"Time":"2018-10-15T21:00:38.738631-04:00","Action":"pass","Package":"strings","Test":"ExampleTrimRightFunc","Elapsed":0}`,
ActionPass, "strings", "ExampleTrimRightFunc", "", false, false, false,
},
{
// 6
`{"Time":"2018-10-15T23:00:27.929094-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"}`,
ActionOutput,
"github.com/astromail/rover/tests",
"",
"2018/10/15 23:00:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n",
false,
false,
true,
},
{
// 7
`{"Time":"2018-10-15T23:00:28.430825-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"PASS\n"}`,
ActionOutput, "github.com/astromail/rover/tests", "", "PASS\n", false, false, true,
},
{
// 8
`{"Time":"2018-10-15T23:00:28.432239-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"ok \tgithub.com/astromail/rover/tests\t0.530s\n"}`,
ActionOutput,
"github.com/astromail/rover/tests",
"",
"ok \tgithub.com/astromail/rover/tests\t0.530s\n",
false,
false,
true,
},
{
// 9
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n"}`,
ActionOutput,
"github.com/mfridman/srfax",
"",
"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n",
false,
false,
true,
},
{
// 10
`{"Time":"2023-05-28T18:36:01.446915-04:00","Action":"start","Package":"github.com/pressly/goose/v4/internal/sqlparser"}`,
ActionStart, "github.com/pressly/goose/v4/internal/sqlparser", "", "", false, false, false,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
if e.Action != tc.action {
t.Errorf("wrong action: got %q, want %q", e.Action, tc.action)
}
if e.Package != tc.pkg {
t.Errorf("wrong pkg name: got %q, want %q", e.Package, tc.pkg)
}
if e.Output != tc.output {
t.Errorf("wrong output: got %q, want %q", e.Output, tc.output)
}
if e.Test != tc.test {
t.Errorf("wrong test name: got %q, want %q", e.Test, tc.test)
}
if e.LastLine() != tc.lastLine {
t.Errorf("failed lastLine check: got %v, want %v", e.LastLine(), tc.lastLine)
}
if e.DiscardOutput() != tc.discardOutput {
t.Errorf("failed discard check: got %v, want %v", e.DiscardOutput(), tc.discardOutput)
}
if e.DiscardEmptyTestOutput() != tc.discardEmptyTestOutput {
t.Errorf("failed discard empty test output check: got %v, want %v", e.DiscardEmptyTestOutput(), tc.discardOutput)
}
if t.Failed() {
t.Logf("failed event: %v", tc.raw)
}
})
}
}
func TestCachedEvent(t *testing.T) {
t.Parallel()
tt := []struct {
raw string
cached bool
}{
{
// 0
`{"Time":"2018-10-24T08:30:14.566611-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t(cached)\n"}`,
true,
},
{
// 1
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n"}`,
true,
},
{
// 2
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"github.com/mfridman/srfax\t(cached)"}`,
false,
},
{
// 3
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"(cached)"}`,
false,
},
{
// 4
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":""}`,
false,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
got := e.IsCached()
want := tc.cached
if got != want {
t.Errorf("got non-cached output (%t), want cached output (%t)", got, want)
t.Logf("input: %v", tc.raw)
}
})
}
}
func TestCoverEvent(t *testing.T) {
t.Parallel()
var zero float64
tt := []struct {
raw string
cover bool
coverage float64
}{
{
// 0
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 28.8% of statements\n"}`, true, 28.8,
},
{
// 1
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 100.0% of statements\n"}`, true, 100.0,
},
{
// 2
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 0.0% of statements\n"}`, true, zero,
},
{
// 3
`{"Time":"2018-10-24T09:25:59.855826-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t0.027s\tcoverage: 87.5% of statements\n"}`, true, 87.5,
},
{
// 4
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: 1000.0% of statements\n"}`, true, zero,
},
{
// 5
`{"Time":"2018-10-24T08:48:23.634909-04:00","Action":"output","Package":"github.com/mfridman/srfax","Output":"ok \tgithub.com/mfridman/srfax\t(cached)\tcoverage: .0% of statements\n"}`, false, zero,
},
{
// 6
`{"Time":"2022-05-23T23:07:54.485803-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t0.516s\tcoverage: 34.5% of statements in ./...\n"}`, true, 34.5,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
f, ok := e.Cover()
if ok != tc.cover {
t.Errorf("got (%t) non-coverage event, want %t", ok, tc.cover)
}
if f != tc.coverage {
t.Errorf("got wrong percentage for coverage %v, want %v", f, tc.coverage)
}
if t.Failed() {
t.Logf("input: %v", tc.raw)
}
})
}
}
func TestNoTestFiles(t *testing.T) {
t.Parallel()
// [no test files]
tt := []struct {
raw string
noTestFiles bool
}{
{
// 0
`{"Time": "2018-10-28T00:06:53.478265-04:00", "Action": "output", "Package": "github.com/astromail/rover", "Output": "? \tgithub.com/astromail/rover\t[no test files]\n"}`, true,
},
{
// 1
`{"Time": "2018-10-28T00:06:53.511804-04:00", "Action": "output", "Package": "github.com/astromail/rover/cmd/roverd", "Output": "? \tgithub.com/astromail/rover/cmd/roverd\t[no test files]\n"}`, true,
},
{
// 2
`{"Time": "2018-10-28T00:06:53.511804-04:00", "Action": "output", "Package": "github.com/astromail/rover/cmd/roverd", "Output": " \tgithub.com/astromail/rover/cmd/roverd\t[no test files]"}`, false,
},
{
// 3
`{"Time": "2018-10-28T00:06:53.511804-04:00", "Action": "output", "Package": "github.com[no test files]\n"}`, false,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
got := e.NoTestFiles()
want := tc.noTestFiles
if got != want {
t.Errorf("got (%t), want (%t) for no test files", got, want)
t.Logf("input: %v", tc.raw)
}
})
}
}
func TestNoTestsToRun(t *testing.T) {
t.Parallel()
// [no test files]
// This is testing the "package" level
tt := []struct {
raw string
noTests bool
}{
{
// 0
`{"Time":"2018-10-28T18:20:47.18358917-04:00","Action":"output","Package":"github.com/awesome/james","Output":"ok \tgithub.com/awesome/james\t(cached) [no tests to run]\n"}`, true,
},
{
// 1
`{"Time": "2018-10-28T00:06:53.511804-04:00", "Action": "output", "Package": "github.com/astromail/rover/cmd/roverd", "Output": "? \tgithub.com/astromail/rover/cmd/roverd\t[no test files]\n"}`, false,
},
{
`{"Time":"2018-10-29T09:31:49.853255-04:00","Action":"output","Package":"github.com/outerspace/v1/tests","Test":"TestSatelliteTransponder","Output":"testing: warning: no tests to run\n"}`, false,
},
{
`{"Time":"2018-10-28T18:20:47.18358917-04:00","Action":"output","Package":"github.com/a/tests","Output":"ok \tgithub.com/a/tests\t(cached) [no tests to run]\n"}`, true,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
got := e.NoTestsToRun()
want := tc.noTests
if got != want {
t.Errorf("got (%t), want (%t) for no tests to run", got, want)
t.Logf("input: %v", tc.raw)
}
})
}
}
func TestNoTestsWarn(t *testing.T) {
t.Parallel()
// [no test files]
// This is testing the "test" level only
tt := []struct {
raw string
wanNoTests bool
}{
{
// 0
`{"Time":"2018-10-28T18:20:47.18358917-04:00","Action":"output","Package":"github.com/awesome/james","Output":"ok \tgithub.com/awesome/james\t(cached) [no tests to run]\n"}`, false,
},
{
// 1
`{"Time": "2018-10-28T00:06:53.511804-04:00", "Action": "output", "Package": "github.com/astromail/rover/cmd/roverd", "Output": "? \tgithub.com/astromail/rover/cmd/roverd\t[no test files]\n"}`, false,
},
{
// 2
`{"Time":"2018-10-29T09:31:49.853255-04:00","Action":"output","Package":"github.com/outerspace/v1/tests","Test":"TestSatelliteTransponder","Output":"testing: warning: no tests to run\n"}`, true,
},
{
// 3
`{"Time":"2018-10-28T18:20:47.245658814-04:00","Action":"output","Package":"github.com/abc/tests","Output":"testing: warning: no tests to run\n"}`, false,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("event_%d", i), func(t *testing.T) {
e, err := NewEvent([]byte(tc.raw))
require.NoError(t, err)
got := e.NoTestsWarn()
want := tc.wanNoTests
if got != want {
t.Errorf("got (%t), want (%t) for warn no tests to run", got, want)
t.Logf("input: %v", tc.raw)
}
})
}
}
func TestActionString(t *testing.T) {
t.Parallel()
tt := []struct {
Action
want string
}{
{ActionRun, "RUN"},
{ActionPause, "PAUSE"},
{ActionCont, "CONT"},
{ActionPass, "PASS"},
{ActionFail, "FAIL"},
{ActionOutput, "OUTPUT"},
{ActionSkip, "SKIP"},
{ActionBench, "BENCH"},
{ActionStart, "START"},
}
for _, tc := range tt {
upper := strings.ToUpper(tc.String())
if upper != tc.want {
t.Errorf("got %q, want %q", upper, tc.want)
}
}
}
func TestDiscardOutput(t *testing.T) {
t.Parallel()
// Table test for JSON events that should be discarded
tt := []string{
`{"Time":"2018-11-24T23:18:44.381562-05:00","Action":"output","Package":"time","Test":"TestMonotonicOverflow","Output":"=== RUN TestMonotonicOverflow\n"}`,
`{"Time":"2018-10-28T23:41:31.939308-04:00","Action":"output","Package":"github.com/mfridman/tparse/parse","Test":"TestNewEvent","Output":"=== PAUSE TestNewEvent\n"}`,
`{"Time":"2022-05-20T20:16:06.761846-04:00","Action":"output","Package":"github.com/pressly/goose/v3/tests/e2e","Test":"TestNowAllowMissingUpByOne","Output":"=== CONT TestNowAllowMissingUpByOne\n"}`,
}
for _, tc := range tt {
e, err := NewEvent([]byte(tc))
require.NoError(t, err)
if e.DiscardOutput() != true {
t.Errorf("%s - %s failed discard check: got:%v, want:%v", e.Package, e.Test, e.DiscardOutput(), true)
}
}
}
tparse-0.18.0/parse/package.go 0000664 0000000 0000000 00000005264 15052652102 0016137 0 ustar 00root root 0000000 0000000 package parse
import "time"
// Package is the representation of a single package being tested. The
// summary field is an event that contains all relevant information about the
// package, namely Package (name), Elapsed and Action (big pass or fail).
type Package struct {
Summary *Event
Tests []*Test
// StartTime is the time the package started running. This is only available
// in go1.20 and above.
StartTime time.Time
// NoTestFiles indicates whether the package contains tests: [no test files]
// This only occurs at the package level
NoTestFiles bool
// NoTests indicates a package contains one or more files with no tests. This doesn't
// necessarily mean the file is empty or that the package doesn't have any tests.
// Unfortunately go test marks the package summary with [no tests to run].
NoTests bool
// NoTestSlice holds events that contain "testing: warning: no tests to run" and
// a non-empty test name.
NoTestSlice []*Event
// Cached indicates whether the test result was obtained from the cache.
Cached bool
// Cover reports whether the package contains coverage (go test run with -cover)
Cover bool
Coverage float64
// HasPanic marks the entire package as panicked. Game over.
HasPanic bool
// Once a package has been marked HasPanic all subsequent events are added to PanicEvents.
PanicEvents []*Event
// HasDataRace marks the entire package as having a data race.
HasDataRace bool
// DataRaceTests captures an individual test names as having a data race.
DataRaceTests []string
// HasFailedBuildOrSetup marks the package as having a failed build or setup.
// Example: [build failed] or [setup failed]
HasFailedBuildOrSetup bool
}
// newPackage initializes and returns a Package.
func newPackage() *Package {
return &Package{
Summary: &Event{},
Tests: []*Test{},
}
}
// AddEvent adds the event to a test based on test name.
func (p *Package) AddEvent(event *Event) {
var t *Test
if t = p.GetTest(event.Test); t == nil {
// Test does not exist, add it to pkg.
t = &Test{
Name: event.Test,
Package: event.Package,
}
p.Tests = append(p.Tests, t)
}
t.Events = append(t.Events, event)
}
// GetTest returns a test based on given name, if no test is found
// return nil
func (p *Package) GetTest(name string) *Test {
for _, t := range p.Tests {
if t.Name == name {
return t
}
}
return nil
}
// TestsByAction returns all tests that identify as one of the following
// actions: pass, skip or fail.
//
// An empty slice if returned if there are no tests.
func (p *Package) TestsByAction(action Action) []*Test {
var tests []*Test
for _, t := range p.Tests {
if t.Status() == action {
tests = append(tests, t)
}
}
return tests
}
tparse-0.18.0/parse/package_slice.go 0000664 0000000 0000000 00000002353 15052652102 0017312 0 ustar 00root root 0000000 0000000 package parse
import (
"sort"
)
type PackageSorter func([]*Package) sort.Interface
type PackageSlice []*Package
type byCoverage struct{ PackageSlice }
type byElapsed struct{ PackageSlice }
// SortByPackageName sorts packages in ascending alphabetical order.
func SortByPackageName(packages []*Package) sort.Interface { return PackageSlice(packages) }
func (packages PackageSlice) Len() int { return len(packages) }
func (packages PackageSlice) Swap(i, j int) {
packages[i], packages[j] = packages[j], packages[i]
}
func (packages PackageSlice) Less(i, j int) bool {
return packages[i].Summary.Package < packages[j].Summary.Package
}
// SortByCoverage sorts packages in descending order of code coverage.
func SortByCoverage(packages []*Package) sort.Interface { return byCoverage{packages} }
func (packages byCoverage) Less(i, j int) bool {
return packages.PackageSlice[i].Coverage > packages.PackageSlice[j].Coverage
}
// SortByElapsed sorts packages in descending order of elapsed time per package.
func SortByElapsed(packages []*Package) sort.Interface { return byElapsed{packages} }
func (packages byElapsed) Less(i, j int) bool {
return packages.PackageSlice[i].Summary.Elapsed > packages.PackageSlice[j].Summary.Elapsed
}
tparse-0.18.0/parse/process.go 0000664 0000000 0000000 00000021115 15052652102 0016213 0 ustar 00root root 0000000 0000000 package parse
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
)
// ErrNotParsable indicates the event line was not parsable.
var ErrNotParsable = errors.New("failed to parse")
// Process is the entry point to parse. It consumes a reader
// and parses go test output in JSON format until EOF.
//
// Note, Process will attempt to parse up to 50 lines before returning an error.
func Process(r io.Reader, optionsFunc ...OptionsFunc) (*GoTestSummary, error) {
option := &options{}
for _, f := range optionsFunc {
f(option)
}
summary := &GoTestSummary{
Packages: make(map[string]*Package),
}
noisy := []string{
// 1. Filter out noisy output, such as === RUN, === PAUSE, etc.
updatePrefixRun,
updatePrefixPause,
updatePrefixCont,
updatePrefixPass,
updatePrefixSkip,
// 2. Filter out report output, such as --- PASS: and --- SKIP:
resultPrefixPass,
resultPrefixSkip,
}
isNoisy := func(e *Event) bool {
output := strings.TrimSpace(e.Output)
// If the event is a big pass or fail, we can safely discard it. These are typically the
// lines preceding the package summary line. For example:
//
// PASS
// ok fmt 0.144s
if e.Test == "" && (output == bigPass || output == bigFail) {
return true
}
for _, prefix := range noisy {
if strings.HasPrefix(output, prefix) {
return true
}
}
return false
}
sc := bufio.NewScanner(r)
var started bool
var badLines int
for sc.Scan() {
// Scan up-to 50 lines for a parsable event, if we get one, expect
// no errors to follow until EOF.
e, err := NewEvent(sc.Bytes())
if err != nil {
// We failed to parse a go test JSON event, but there are special cases for failed
// builds, setup, etc. Let special case these and bubble them up in the summary
// if the output belongs to a package.
summary.AddRawEvent(sc.Text())
badLines++
if started || badLines > 50 {
var syntaxError *json.SyntaxError
if errors.As(err, &syntaxError) {
err = fmt.Errorf("line %d JSON error: %s: %w", badLines, syntaxError.Error(), ErrNotParsable)
if option.debug {
// In debug mode we can surface a more verbose error message which
// contains the current line number and exact JSON parsing error.
fmt.Fprintf(os.Stderr, "debug: %s", err.Error())
}
}
return nil, err
}
if option.follow && option.w != nil {
fmt.Fprintf(option.w, "%s\n", sc.Bytes())
}
continue
}
started = true
// TODO(mf): when running tparse locally it's very useful to see progress for long-running
// test suites. Since we have access to the event we can send it on a chan
// or just directly update a spinner-like component. This cannot be run with the
// follow option. Lastly, need to consider what local vs CI behavior would be like.
// Depending on how often the frames update, this could cause a lot of noise, so maybe
// we need to expose an interval option, so in CI it would update infrequently.
// Optionally, as test output is piped to us, we write the plain
// text Output as if go test was run without the -json flag.
if (option.follow || option.followVerbose) && option.w != nil {
if !option.followVerbose && isNoisy(e) {
continue
}
fmt.Fprint(option.w, e.Output)
}
// Progress is a special case of follow, where we only print the
// progress of the test suite, but not the output.
if option.progress && option.w != nil {
printProgress(option.progressOutput, e, summary.Packages)
}
// TODO(mf): special case build output for now. Need to understand how to better handle this
// But we don't want to swallow important build errors. There is a class of build output
// that is bengin like: https://github.com/golang/go/issues/61229
//
// Example:
// ld: warning: '.../go.o' has malformed LC_DYSYMTAB, expected 92 undefined symbols to start at index 15983, found 102 undefined symbol
//
// TL;DR - output ALL build output to stderr and exclude it from being added to test events
if e.ImportPath != "" {
if e.Output != "" {
fmt.Fprint(os.Stderr, e.Output)
}
continue
}
summary.AddEvent(e)
}
if err := sc.Err(); err != nil {
return nil, fmt.Errorf("received scanning error: %w", err)
}
// Entire input has been scanned and no go test JSON output was found.
if !started {
return nil, ErrNotParsable
}
return summary, nil
}
// printProgress prints a single summary line for each PASS or FAIL package.
// This is useful for long-running test suites.
func printProgress(w io.Writer, e *Event, summary map[string]*Package) {
if !e.LastLine() {
return
}
action := e.Action
var suffix string
if pkg, ok := summary[e.Package]; ok {
if pkg.NoTests {
suffix = " [no tests to run]"
action = ActionSkip
}
if pkg.NoTestFiles {
suffix = " [no test files]"
action = ActionSkip
}
}
// Normal go test output will print the package summary line like so:
//
// FAIL
// FAIL github.com/pressly/goose/v4/internal/sqlparser 0.577s
//
// PASS
// ok github.com/pressly/goose/v4/internal/sqlparser 0.349s
//
// ? github.com/pressly/goose/v4/internal/check [no test files]
//
// testing: warning: no tests to run
// PASS
// ok github.com/pressly/goose/v4/pkg/source 0.382s [no tests to run]
//
// We modify this output slightly so it's more consistent and easier to parse.
fmt.Fprintf(w, "[%s]\t%10s\t%s%s\n",
strings.ToUpper(action.String()),
strconv.FormatFloat(e.Elapsed, 'f', 2, 64)+"s",
e.Package,
suffix,
)
}
type GoTestSummary struct {
Packages map[string]*Package
}
func (s *GoTestSummary) AddRawEvent(str string) {
if strings.HasPrefix(str, "FAIL") {
ss := failedBuildOrSetupRe.FindStringSubmatch(str)
if len(ss) == 3 {
pkgName, failMessage := strings.TrimSpace(ss[1]), strings.TrimSpace(ss[2])
pkg, ok := s.Packages[pkgName]
if !ok {
pkg = newPackage()
s.Packages[pkgName] = pkg
}
pkg.Summary.Package = pkgName
pkg.Summary.Action = ActionFail
pkg.Summary.Output = failMessage
pkg.HasFailedBuildOrSetup = true
}
}
}
func (s *GoTestSummary) AddEvent(e *Event) {
// Discard noisy output such as "=== CONT", "=== RUN", etc. These add
// no value to the go test output, unless you care to follow how often
// tests are paused and for what duration.
if e.Action == ActionOutput && e.DiscardOutput() {
return
}
pkg, ok := s.Packages[e.Package]
if !ok {
pkg = newPackage()
s.Packages[e.Package] = pkg
}
// Capture the start time of the package. This is only available in go1.20 and above.
if e.Action == ActionStart {
pkg.StartTime = e.Time
return
}
// Special case panics.
if e.IsPanic() {
pkg.HasPanic = true
pkg.Summary.Action = ActionFail
pkg.Summary.Package = e.Package
pkg.Summary.Test = e.Test
}
// Short circuit output when panic is detected.
if pkg.HasPanic {
pkg.PanicEvents = append(pkg.PanicEvents, e)
return
}
if e.LastLine() {
pkg.Summary = e
return
}
// Parse the raw output to add additional metadata to Package.
switch {
case e.IsRace():
pkg.HasDataRace = true
if e.Test != "" {
pkg.DataRaceTests = append(pkg.DataRaceTests, e.Test)
}
case e.IsCached():
pkg.Cached = true
case e.NoTestFiles():
pkg.NoTestFiles = true
// Manually mark [no test files] as "pass", because the go test tool reports the
// package Summary action as "skip".
// TODO(mf): revisit this behavior?
pkg.Summary.Package = e.Package
pkg.Summary.Action = ActionPass
case e.NoTestsWarn():
// One or more tests within the package contains no tests.
pkg.NoTestSlice = append(pkg.NoTestSlice, e)
case e.NoTestsToRun():
// Only packages marked as "pass" will contain a summary line appended with [no tests to run].
// This indicates one or more tests is marked as having no tests to run.
pkg.NoTests = true
pkg.Summary.Package = e.Package
pkg.Summary.Action = ActionPass
default:
if cover, ok := e.Cover(); ok {
pkg.Cover = true
pkg.Coverage = cover
}
}
// We captured all the necessary package-level information, if the event
// is output and does not have a test name, discard it.
if e.DiscardEmptyTestOutput() {
return
}
pkg.AddEvent(e)
}
func (s *GoTestSummary) GetSortedPackages(sorter PackageSorter) []*Package {
packages := make([]*Package, 0, len(s.Packages))
for _, pkg := range s.Packages {
packages = append(packages, pkg)
}
sort.Sort(sorter(packages))
return packages
}
func (s *GoTestSummary) ExitCode() int {
for _, pkg := range s.Packages {
switch {
case pkg.HasFailedBuildOrSetup:
return 2
case pkg.HasPanic, pkg.HasDataRace:
return 1
case len(pkg.DataRaceTests) > 0:
return 1
case pkg.Summary.Action == ActionFail:
return 1
}
}
return 0
}
tparse-0.18.0/parse/process_options.go 0000664 0000000 0000000 00000001402 15052652102 0017763 0 ustar 00root root 0000000 0000000 package parse
import (
"io"
)
type options struct {
w io.Writer
follow bool
followVerbose bool
debug bool
progress bool
progressOutput io.Writer
}
type OptionsFunc func(o *options)
func WithFollowOutput(b bool) OptionsFunc {
return func(o *options) { o.follow = b }
}
func WithFollowVersboseOutput(b bool) OptionsFunc {
return func(o *options) { o.followVerbose = b }
}
func WithWriter(w io.Writer) OptionsFunc {
return func(o *options) { o.w = w }
}
func WithDebug() OptionsFunc {
return func(o *options) { o.debug = true }
}
func WithProgress(b bool) OptionsFunc {
return func(o *options) { o.progress = b }
}
func WithProgressOutput(w io.Writer) OptionsFunc {
return func(o *options) { o.progressOutput = w }
}
tparse-0.18.0/parse/test.go 0000664 0000000 0000000 00000002245 15052652102 0015517 0 ustar 00root root 0000000 0000000 package parse
import (
"sort"
)
// Test represents a single, unique, package test.
type Test struct {
Name string
Package string
Events []*Event
}
// Elapsed indicates how long a given test ran (in seconds), by scanning for the largest
// elapsed value from all events.
func (t *Test) Elapsed() float64 {
var f float64
for _, e := range t.Events {
if e.Elapsed > f {
f = e.Elapsed
}
}
return f
}
// Status reports the outcome of the test represented as a single Action: pass, fail or skip.
func (t *Test) Status() Action {
// sort by time and scan for an action in reverse order.
// The first action we come across (in reverse order) is
// the outcome of the test, which will be one of pass|fail|skip.
t.SortEvents()
for i := len(t.Events) - 1; i >= 0; i-- {
switch t.Events[i].Action {
case ActionPass:
return ActionPass
case ActionSkip:
return ActionSkip
case ActionFail:
return ActionFail
}
}
return ActionFail
}
// SortEvents sorts test events by elapsed time in ascending order, i.e., oldest to newest.
func (t *Test) SortEvents() {
sort.Slice(t.Events, func(i, j int) bool {
return t.Events[i].Time.Before(t.Events[j].Time)
})
}
tparse-0.18.0/scripts/ 0000775 0000000 0000000 00000000000 15052652102 0014563 5 ustar 00root root 0000000 0000000 tparse-0.18.0/scripts/release-notes.sh 0000775 0000000 0000000 00000002644 15052652102 0017676 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -euo pipefail
# Check if the required argument is provided
if [ $# -lt 1 ]; then
echo "Usage: $0 []"
exit 1
fi
version="$1"
changelog_file="${2:-CHANGELOG.md}"
# Check if the changelog file exists
if [ ! -f "$changelog_file" ]; then
echo "Error: $changelog_file does not exist"
exit 1
fi
CAPTURE=0
items=""
# Read the changelog file line by line
while IFS= read -r LINE; do
# Stop capturing when we reach the next version sections
if [[ "${LINE}" == "##"* ]] && [[ "${CAPTURE}" -eq 1 ]]; then
break
fi
# Stop capturing when we reach the Unreleased section
if [[ "${LINE}" == "[Unreleased]"* ]]; then
break
fi
# Start capturing when we reach the specified version section
if [[ "${LINE}" == "## [${version}]"* ]] && [[ "${CAPTURE}" -eq 0 ]]; then
CAPTURE=1
continue
fi
# Capture the lines between the specified version and the next version
if [[ "${CAPTURE}" -eq 1 ]]; then
# Ignore empty lines
if [[ -z "${LINE}" ]]; then
continue
fi
items+="$(echo "${LINE}" | xargs -0)"
# Add a newline between each item
if [[ -n "$items" ]]; then
items+=$'\n'
fi
fi
done <"${changelog_file}"
if [[ -n "$items" ]]; then
cat <