Skip to content

Commit

Permalink
Fixes #33: Non-zero exit code if one or more tests failed (#34)
Browse files Browse the repository at this point in the history
* Fixes #33: Non-zero exit code if one or more tests failed

* Added -nofail option to force a 0 exit code in case of a test failure.

* Updated readme with the -nofail flag
  • Loading branch information
Janos Bonic authored Feb 13, 2022
1 parent b92e541 commit 5aad83d
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 6 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Tadam, your tests will now show up in a beautifully formatted fashion. Plug it i
- [Add your own CI](#add-your-own-ci)
- [FAQ](#faq)
- [How do I make the output less verbose?](#how-do-i-make-the-output-less-verbose)
- [How do I format the log lines within a test?](#how-do)
- [How do I format the log lines within a test?](#how-do-i-format-the-log-lines-within-a-test)
- [Why does gotestfmt exit with a non-zero status?](#why-does-gotestfmt-exit-with-a-non-zero-status)
- [Can I use gotestfmt without `-json`?](#can-i-use-gotestfmt-without--json)
- [Does gotestfmt work with Ginkgo?](#does-gotestfmt-work-with-ginkgo)
- [I don't like `gotestfmt`. What else can I use?](#i-dont-like-gotestfmt-what-else-can-i-use)
Expand Down Expand Up @@ -299,6 +300,10 @@ The formatter will be called for each individual test case separately and the en

You can find a sample formatter written in Go in [cmd/gotestfmt-formatter/main.go](cmd/gotestfmt-formatter/main.go).

### Why does gotestfmt exit with a non-zero status?

As of version 2.3.0 gotestfmt returns with a non-zero exit status when one or more tests fail. We added this behavior to make sure your CI doesn't pass on failing tests if you forget the `set -euo pipefail` option. You can disable this behavior by passing the `-nofail` parameter in the command line.

### How do I know what the icons mean in the output?

The icons are based on the output of `go test -json`. They map to the values from the [`test2json`](https://pkg.go.dev/cmd/test2json) package (PASS, FAIL, SKIP).
Expand Down
12 changes: 11 additions & 1 deletion cmd/gotestfmt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func main() {
inputFile := "-"
formatter := ""
hide := ""
var nofail bool
var showTestStatus bool

flag.StringVar(
Expand Down Expand Up @@ -129,6 +130,12 @@ func main() {
formatter,
"Absolute path to an external program to format individual test output. This program will be called for each test case with a non-empty output and receive the test case output on stdin. It must produce the final output on stdout.",
)
flag.BoolVar(
&nofail,
"nofail",
nofail,
"Return an exit code of 0 even if one or more tests failed.",
)
flag.Parse()

if ci != "" {
Expand Down Expand Up @@ -171,5 +178,8 @@ func main() {
input = fh
}

format.FormatWithConfig(input, os.Stdout, cfg)
exitCode := format.FormatWithConfigAndExitCode(input, os.Stdout, cfg)
if !nofail {
os.Exit(exitCode)
}
}
30 changes: 26 additions & 4 deletions gotestfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var fs embed.FS

func New(
templateDirs []string,
) (Combined, error) {
) (CombinedExitCode, error) {
downloadsTpl := findTemplate(templateDirs, "downloads.gotpl")

packageTpl := findTemplate(templateDirs, "package.gotpl")
Expand Down Expand Up @@ -55,6 +55,12 @@ type Combined interface {
Formatter
}

// CombinedExitCode contains Combined and adds a function to format with exit code.
type CombinedExitCode interface {
Combined
FormatterExitCode
}

// GoTestFmt implements the classic Format instruction. This is no longer in use.
//
// Deprecated: please use the Formatter interface instead.
Expand All @@ -68,24 +74,40 @@ type Formatter interface {
FormatWithConfig(input io.Reader, target io.WriteCloser, cfg renderer.RenderSettings)
}

// FormatterExitCode contains an extended format function to accept render settings and returns an exit code
type FormatterExitCode interface {
FormatWithConfigAndExitCode(input io.Reader, target io.WriteCloser, cfg renderer.RenderSettings) int
}

type goTestFmt struct {
packageTpl []byte
downloadsTpl []byte
}

func (g *goTestFmt) Format(input io.Reader, target io.WriteCloser) {
g.FormatWithConfig(input, target, renderer.RenderSettings{})
g.FormatWithConfigAndExitCode(input, target, renderer.RenderSettings{})
}

func (g *goTestFmt) FormatWithConfig(input io.Reader, target io.WriteCloser, cfg renderer.RenderSettings) {
_ = g.FormatWithConfigAndExitCode(input, target, cfg)
}

func (g *goTestFmt) FormatWithConfigAndExitCode(input io.Reader, target io.WriteCloser, cfg renderer.RenderSettings) int {
tokenizerOutput := tokenizer.Tokenize(input)
prefixes, downloads, packages := parser.Parse(tokenizerOutput)
result := renderer.RenderWithSettings(prefixes, downloads, packages, g.downloadsTpl, g.packageTpl, cfg)
result, exitCodeChan := renderer.RenderWithSettingsAndExitCode(
prefixes,
downloads,
packages,
g.downloadsTpl,
g.packageTpl,
cfg,
)

for {
fragment, ok := <-result
if !ok {
return
return <-exitCodeChan
}
if _, err := target.Write(fragment); err != nil {
panic(fmt.Errorf("failed to write to output: %w", err))
Expand Down
66 changes: 66 additions & 0 deletions renderer/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,72 @@ func RenderWithSettings(
return result
}

// RenderWithSettingsAndExitCode takes the two input channels from the parser and renders them into text output
// fragments as well as an exit code.
func RenderWithSettingsAndExitCode(
prefixes <-chan string,
downloadsChannel <-chan *parser.Downloads,
packagesChannel <-chan *parser.Package,
downloadsTemplate []byte,
packagesTemplate []byte,
settings RenderSettings,
) (<-chan []byte, <-chan int) {
result := make(chan []byte)
exitCodeChan := make(chan int)
go func() {
exitCode := 0
defer func() {
close(result)
exitCodeChan <- exitCode
close(exitCodeChan)
}()
for {
prefix, ok := <-prefixes
if !ok {
break
}
result <- []byte(fmt.Sprintf("%s\n", prefix))
}

for {
downloads, ok := <-downloadsChannel
if !ok {
break
}
if downloads.Failed {
exitCode = 1
}
result <- renderTemplate(
"downloads.gotpl",
downloadsTemplate,
Downloads{
downloads,
settings,
},
)
}

for {
pkg, ok := <-packagesChannel
if !ok {
break
}
if pkg.Result == parser.ResultFail {
exitCode = 1
}
result <- renderTemplate(
"package.gotpl",
packagesTemplate,
Package{
pkg,
settings,
},
)
}
}()
return result, exitCodeChan
}

// Downloads contains the downloads for rendering.
type Downloads struct {
*parser.Downloads
Expand Down

0 comments on commit 5aad83d

Please sign in to comment.