blob: ccb8cf6d141923ce293bbb3e5ed3251f6c8dd58b [file] [log] [blame] [view]
# Proposal: `-json` flag in `go test`
Author(s): Nodir Turakulov <nodir@google.com>
_With initial input by Russ Cox, Caleb Spare, Andrew Gerrand and Minux Ma._
Last updated: 2016-09-14
Discussion at https://golang.org/issue/2981.
* [Abstract](#abstract)
* [Background](#background)
* [Proposal](#proposal)
* [`testing` package](#testing-package)
* [Example output](#example-output)
* [Rationale](#rationale)
* [Compatibility](#compatibility)
* [Implementation](#implementation)
## Abstract
Add `-json` flag to `go test`.
When specified, `go test` stdout is JSON.
## Background
There is a clear need in parsing test and benchmark results by third party
tools, see feedback in https://golang.org/issue/2981.
Currently `go test` output format is suited for humans, but not computers.
Also a change to the current format may break existing programs that parse
`go test` output.
Currently, under certain conditions, `go test` streams test/benchmark results
so a user can see them as they happen.
Also streaming prevents losing data if `go test` crashes.
This proposal attempts to preserve streaming capability in the `-json` mode, so
third party tools interpreting `go test` output can stream results too.
`-json` flag was originally proposed by Russ Cox in
https://golang.org/issue/2981 in 2012. This proposal has several differences.
## Proposal
I propose the following user-visible changes:
* add `-json` flag to `go test`
* `-json`: `go test` stdout is a valid [JSON Text Sequence][rfc7464]
of JSON objects containing test binary artifacts.
Format below.
* `-json -v`: verbose messages are printed to stderr, so stdout contains
only JSON.
* `-json -n`: not supported
* `-json -x`: not supported
* In `testing` package
* Add `type State int` with constants to describe test/benchmark states.
* Add type `JSONResult` for JSON output.
* Change `Cover.CoveredPackages` field type from `string` to `[]string`.
Type definitions and details below.
### `testing` package
```go
// State is one of test/benchmark execution states.
// Implements fmt.Stringer, json.Marshaler and json.Unmarshaler.
type State int
const (
// RUN means a test/benchmark execution has started
RUN State = iota + 1
PASS
FAIL
SKIP
)
// JSONResult structs encoded in JSON are emitted by `go test` if -json flag is
// specified.
type JSONResult struct {
// Configuration is metadata produced by test/benchmark infrastructure.
// The interpretation of a key/value pair is up to tooling, but the key/value
// pair describes all test/benchmark results that follow,
// until overwritten by a JSONResult with a non-empty Configuration field.
//
// The key begins with a lowercase character (as defined by unicode.IsLower),
// contains no space characters (as defined by unicode.IsSpace)
// nor upper case characters (as defined by unicode.IsUpper).
// Conventionally, multiword keys are written with the words separated by hyphens,
// as in cpu-speed.
Configuration map[string]string `json:",omitempty"`
// Package is a full name of the package containing the test/benchmark.
// It is zero iff Name is zero.
Package string `json:",omitempty"`
// Name is the name of the test/benchmark that this JSONResult is about.
// It can be empty if JSONResult describes global state, such as
// Configuration or Stdout/Stderr.
Name string `json:",omitempty"`
// State is the current state of the test/benchmark.
// It is non-zero iff Name is non-zero.
State State `json:",omitempty"`
// Procs is the value of runtime.GOMAXPROCS for this test/benchmark run.
// It is specified only in the first JSONResult of a test/benchmark.
Procs int `json:",omitempty"`
// Log is log created by calling Log or Logf functions of *T or *B.
// A JSONResult with Log is emitted by go test as soon as possible.
// First occurrence of test/benchmark does not contain logs.
Log string `json:",omitempty"`
// Benchmark contains benchmark-specific details.
// It is emitted in the final JSONResult of a benchmark with a terminal
// State if the benchmark does not have sub-benchmarks.
Benchmark *BenchmarkResult `json:",omitempty"`
// CoverageMode is coverage mode that was used to run these tests.
CoverageMode string `json:",omitempty"
// TotalStatements is the number of statements checked for coverage.
TotalStatements int64 `json:",omitempty"`
// ActiveStatements is the number of statements covered by tests, examples
// or benchmarks.
ActiveStatements int64 `json:",omitempty"`
// CoveragedPackages is full names of packages included in coverage.
CoveredPackages []string `json:",omitempty"`
// Stdout is text written by the test binary directly to os.Stdout.
// If this field is non-zero, all others are zero.
Stdout string `json:",omitempty"`
// Stderr is text written by test binary directly to os.Stderr.
// If this field is non-zero, all others are zero.
Stderr string `json:",omitempty"`
}
```
### Example output
Here is an example of `go test -json` output.
It is simplified and commented for the convenience of the reader;
in practice it will be unindented and will contain JSON Text Sequence separators
and no comments.
```json
// go test emits environment configuration
{
"Configuration": {
"commit": "7cd9055",
"commit-time": "2016-02-11T13:25:45-0500",
"goos": "darwin",
"goarch": "amd64",
"cpu": "Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz",
"cpu-count": "8",
"cpu-physical-count": "4",
"os": "Mac OS X 10.11.3",
"mem": "16 GB"
}
}
// TestFoo started
{
"Package": "github.com/user/repo",
"Name": "TestFoo",
"State": "RUN",
"Procs": 4
}
// A line was written directly to os.Stdout
{
"Package": "github.com/user/repo",
"Stderr": "Random string written directly to os.Stdout\n"
}
// TestFoo passed
{
"Package": "github.com/user/repo",
"Name": "TestFoo",
"State": "PASS",
}
// TestBar started
{
"Package": "github.com/user/repo",
"Name": "TestBar",
"State": "RUN",
"Procs": 4
}
// TestBar logged a line
{
"Package": "github.com/user/repo",
"Name": "TestBar",
"State": "RUN",
"Log": "some test output"
}
// TestBar failed
{
"Package": "github.com/user/repo",
"Name": "TestBar",
"State": "FAIL"
}
// TestQux started
{
"Package": "github.com/user/repo",
"Name": "TestQux",
"State": "RUN",
"Procs": 4
}
// TestQux calls T.Fatal("bug")
{
"Package": "github.com/user/repo",
"Name": "TestBar",
"State": "RUN",
"Log": "bug"
}
{
"Package": "github.com/user/repo",
"Name": "TestQux",
"State": "FAIL"
}
// TestComposite started
{
"Package": "github.com/user/repo",
"Name": "TestComposite",
"State": "RUN",
"Procs": 4
}
// TestComposite/A=1 subtest started
{
"Package": "github.com/user/repo",
"Name": "TestComposite/A=1",
"State": "RUN",
"Procs": 4
}
// TestComposite/A=1 passed
{
"Package": "github.com/user/repo",
"Name": "TestComposite/A=1",
"State": "PASS",
}
// TestComposite passed
{
"Package": "github.com/user/repo",
"Name": "TestComposite",
"State": "PASS",
}
// Example1 started
{
"Package": "github.com/user/repo",
"Name": "Example1",
"State": "RUN",
"Procs": 4
}
// Example1 passed
{
"Package": "github.com/user/repo",
"Name": "Example1",
"State": "PASS"
}
// BenchmarkRun started
{
"Package": "github.com/user/repo",
"Name": "BenchmarkBar",
"State": "RUN",
"Procs": 4
}
// BenchmarkRun passed
{
"Package": "github.com/user/repo",
"Name": "BenchmarkBar",
"State": "PASS",
"Benchmark": {
"T": 1000000,
"N": 1000,
"Bytes": 100,
"MemAllocs": 10,
"MemBytes": 10
}
}
// BenchmarkComposite started
{
"Package": "github.com/user/repo",
"Name": "BenchmarkComposite",
"State": "RUN",
"Procs": 4
}
// BenchmarkComposite/A=1 started
{
"Package": "github.com/user/repo",
"Name": "BenchmarkComposite/A=1",
"State": "RUN",
"Procs": 4
}
// BenchmarkComposite/A=1 passed
{
"Package": "github.com/user/repo",
"Name": "BenchmarkComposite/A=1",
"State": "PASS",
"Benchmark": {
"T": 1000000,
"N": 1000,
"Bytes": 100,
"MemAllocs": 10,
"MemBytes": 10
}
}
// BenchmarComposite passed
{
"Package": "github.com/user/repo",
"Name": "BenchmarComposite",
"State": "PASS"
}
// Total coverage information in the end.
{
"CoverageMode": "set",
"TotalStatements": 1000,
"ActiveStatements": 900,
"CoveredPackages": [
"github.com/user/repo"
]
}
```
## Rationale
Alternatives:
* Add `-format` and `-benchformat` flags proposed in
https://github.com/golang/go/issues/12826.
While this is simpler to implement, users will have to do more work to
specify format and then parse it.
Trade offs:
* I propose to make `-json` mutually exclusive with `-n` and `-x` flags.
These flags belong to `go build` subcommand while this proposal is scoped
to `go test`.
Supporting the flags would require adding JSON output knowledge to
`go/build.go`.
* `JSONResult.Benchmark.T` provides duration of a benchmark run, but there is
not an equivalent for a test run.
This is a trade off for `JSONResult` simplicity.
We don't have to define `TestResult` because `JSONResult` is enough to
describe a test result.
Currently `go test` does not provide test timing info, so the proposal is
consistent with the current `go test` output.
## Compatibility
The only backwards incompatibility is changing `testing.Cover.CoveredPackages`
field type, but `testing.Cover` is not covered by Go 1 compatibility
guidelines.
## Implementation
Most of the work would be done by the author of this proposal.
The goal is to get agreement on this proposal and to complete the work
before the 1.8 freeze date.
[testStreamOutput]: https://github.com/golang/go/blob/0b248cea169a261cd0c2db8c014269cca5a170c4/src/cmd/go/test.go#L361-L369
[rfc7464]: https://tools.ietf.org/html/rfc7464