Skip to content

Commit

Permalink
Display file:line definition location of failed and skipped tests.
Browse files Browse the repository at this point in the history
We already display file:line info corresponding with the stack of test
failures, but in cases where the failure happened in setup or teardown
we don't have any entry in the stack that points back to the test
definition. This commit fixes #11.

We have never been able to show file:line info corresponding to skipped
tests. This commit fixes #15.

The reason we want to output file:line info is because the GoLand IDE
lacks full support for gunit but what it does do is parse file:line
log entries in the test runner console as links for quick navigation
back to the source code. So, we are taking advantage of a nice built-in
feature to make up for others that are missing. See #12.
  • Loading branch information
mdwhatcott committed Mar 9, 2018
1 parent 7e2d9ec commit 8e7b5a5
Show file tree
Hide file tree
Showing 13 changed files with 555 additions and 44 deletions.
30 changes: 12 additions & 18 deletions failure_report.go
Expand Up @@ -5,18 +5,21 @@ import (
"fmt"
"runtime"
"strings"

"github.com/smartystreets/gunit/scan"
)

type failureReport struct {
Stack []string
Method string
Fixture string
Package string
Failure string
Stack []string
Method string
Fixture string
Package string
Failure string
FileLine string
}

func newFailureReport(failure string) string {
report := &failureReport{Failure: failure}
func newFailureReport(failure, fileLine string) string {
report := &failureReport{Failure: failure, FileLine: fileLine}
report.ScanStack()
return report.String()
}
Expand Down Expand Up @@ -49,25 +52,16 @@ func (this *failureReport) ParseTestName(name string) {
return
}

if method := parts[last]; hasMethodPrefix(method) {
if method := parts[last]; scan.IsTestCase(method) {
this.Method = method
this.Fixture = parts[last-1]
this.Package = strings.Join(parts[0:last-1], ".")
}
}

func hasMethodPrefix(value string) bool {
for _, allowed := range []string{"FocusLongTest", "FocusTest", "LongTest", "Test", "Setup", "Teardown"} {
if strings.HasPrefix(value, allowed) {
return true
}
}
return false
}

func (this failureReport) String() string {
buffer := new(bytes.Buffer)
fmt.Fprintf(buffer, "Method: %s.%s()\n", this.Fixture, this.Method)
fmt.Fprintf(buffer, "(@) %s\n", this.FileLine)
for i, stack := range this.Stack {
fmt.Fprintf(buffer, "(%d): %s\n", len(this.Stack)-i-1, stack)
}
Expand Down
18 changes: 9 additions & 9 deletions fixture.go
Expand Up @@ -25,13 +25,14 @@ import (
// on Fixture.So and the rich set of should-style assertions provided at
// github.com/smartystreets/assertions/should
type Fixture struct {
t testingT
log *bytes.Buffer
verbose bool
t testingT
log *bytes.Buffer
verbose bool
fileLine string
}

func newFixture(t testingT, verbose bool) *Fixture {
return &Fixture{t: t, verbose: verbose, log: &bytes.Buffer{}}
func newFixture(t testingT, verbose bool, fileLine string) *Fixture {
return &Fixture{t: t, verbose: verbose, log: &bytes.Buffer{}, fileLine: fileLine}
}

// So is a convenience method for reporting assertion failure messages,
Expand All @@ -41,7 +42,6 @@ func (this *Fixture) So(actual interface{}, assert assertion, expected ...interf
failure := assert(actual, expected...)
failed := len(failure) > 0
if failed {
this.t.Fail()
this.fail(failure)
}
return !failed
Expand Down Expand Up @@ -81,12 +81,12 @@ func (this *Fixture) Println(a ...interface{}) { fmt.Fprintln(this

// Write implements io.Writer. There are rare times when this is convenient (debugging via `log.SetOutput(fixture)`).
func (this *Fixture) Write(p []byte) (int, error) { return this.log.Write(p) }
func (this *Fixture) Failed() bool { return this.t.Failed() }
func (this *Fixture) Name() string { return this.t.Name() }
func (this *Fixture) Failed() bool { return this.t.Failed() }
func (this *Fixture) Name() string { return this.t.Name() }

func (this *Fixture) fail(failure string) {
this.t.Fail()
this.Print(newFailureReport(failure))
this.Print(newFailureReport(failure, this.fileLine))
}

func (this *Fixture) finalize() {
Expand Down
22 changes: 13 additions & 9 deletions fixture_runner.go
Expand Up @@ -3,27 +3,31 @@ package gunit
import (
"reflect"
"testing"

"github.com/smartystreets/gunit/scan"
)

func newFixtureRunner(fixture interface{}, t *testing.T, parallel bool) *fixtureRunner {
func newFixtureRunner(fixture interface{}, t *testing.T, parallel bool, positions scan.TestCasePositions) *fixtureRunner {
return &fixtureRunner{
parallel: parallel,
setup: -1,
teardown: -1,
outerT: t,
fixtureType: reflect.ValueOf(fixture).Type(),
positions: positions,
}
}

type fixtureRunner struct {
outerT *testing.T
fixtureType reflect.Type

parallel bool
setup int
teardown int
focus []*testCase
tests []*testCase
parallel bool
setup int
teardown int
focus []*testCase
tests []*testCase
positions scan.TestCasePositions
}

func (this *fixtureRunner) ScanFixtureForTestCases() {
Expand All @@ -40,9 +44,9 @@ func (this *fixtureRunner) scanFixtureMethod(methodIndex int, method fixtureMeth
case method.isTeardown:
this.teardown = methodIndex
case method.isFocusTest:
this.focus = append(this.focus, newTestCase(methodIndex, method, this.parallel))
this.focus = append(this.focus, newTestCase(methodIndex, method, this.parallel, this.positions))
case method.isTest:
this.tests = append(this.tests, newTestCase(methodIndex, method, this.parallel))
this.tests = append(this.tests, newTestCase(methodIndex, method, this.parallel, this.positions))
}
}

Expand All @@ -69,4 +73,4 @@ func skipped(cases []*testCase) []*testCase {
test.skipped = true
}
return cases
}
}
9 changes: 8 additions & 1 deletion runner.go
Expand Up @@ -2,7 +2,10 @@ package gunit

import (
"reflect"
"runtime"
"testing"

"github.com/smartystreets/gunit/scan"
)

// Run receives an instance of a struct that embeds *Fixture.
Expand All @@ -22,7 +25,11 @@ func RunSequential(fixture interface{}, t *testing.T) {

func run(fixture interface{}, t *testing.T, parallel bool) {
ensureEmbeddedFixture(fixture, t)
runner := newFixtureRunner(fixture, t, parallel)

_, filename, _, _ := runtime.Caller(2)
positions := scan.LocateTestCases(filename)

runner := newFixtureRunner(fixture, t, parallel, positions)
runner.ScanFixtureForTestCases()
runner.RunTestCases()
}
Expand Down
40 changes: 40 additions & 0 deletions scan/0_parser.go
@@ -0,0 +1,40 @@
package scan

import (
"bytes"
"errors"
"go/ast"
"go/parser"
"go/token"
)

//////////////////////////////////////////////////////////////////////////////

func scanForFixtures(code string) ([]*fixtureInfo, error) {
fileset := token.NewFileSet()
file, err := parser.ParseFile(fileset, "", code, 0)
if err != nil {
return nil, err
}
// ast.Print(fileset, file) // helps with debugging...
return findAndListFixtures(file)
}

func findAndListFixtures(file *ast.File) ([]*fixtureInfo, error) {
collection := newFixtureCollector().Collect(file)
collection = newFixtureMethodFinder(collection).Find(file)
return listFixtures(collection)
}

func listFixtures(collection map[string]*fixtureInfo) ([]*fixtureInfo, error) {
var fixtures []*fixtureInfo
errorMessage := new(bytes.Buffer)

for _, fixture := range collection {
fixtures = append(fixtures, fixture)
}
if errorMessage.Len() > 0 {
return nil, errors.New(errorMessage.String())
}
return fixtures, nil
}
40 changes: 40 additions & 0 deletions scan/1_fixture_collector.go
@@ -0,0 +1,40 @@
package scan

import "go/ast"

type fixtureCollector struct {
candidates map[string]*fixtureInfo
fixtures map[string]*fixtureInfo
}

func newFixtureCollector() *fixtureCollector {
return &fixtureCollector{
candidates: make(map[string]*fixtureInfo),
fixtures: make(map[string]*fixtureInfo),
}
}

func (this *fixtureCollector) Collect(file *ast.File) map[string]*fixtureInfo {
ast.Walk(this, file) // Calls this.Visit(...) recursively which populates this.fixtures
return this.fixtures
}

func (this *fixtureCollector) Visit(node ast.Node) ast.Visitor {
switch t := node.(type) {
case *ast.TypeSpec:
name := t.Name.Name
this.candidates[name] = &fixtureInfo{StructName: name}
return &fixtureValidator{Parent: this, FixtureName: name}
default:
return this
}
}

func (this *fixtureCollector) Validate(fixture string) {
this.fixtures[fixture] = this.candidates[fixture]
delete(this.candidates, fixture)
}

func (this *fixtureCollector) Invalidate(fixture string) {
this.Validate(fixture)
}
31 changes: 31 additions & 0 deletions scan/2_fixture_validator.go
@@ -0,0 +1,31 @@
package scan

import "go/ast"

type fixtureValidator struct {
Parent *fixtureCollector
FixtureName string
}

func (this *fixtureValidator) Visit(node ast.Node) ast.Visitor {
// We start at a TypeSpec and look for an embedded pointer field: `*gunit.Fixture`.
field, isField := node.(*ast.Field)
if !isField {
return this
}
pointer, isPointer := field.Type.(*ast.StarExpr)
if !isPointer {
return this
}

selector, isSelector := pointer.X.(*ast.SelectorExpr)
if !isSelector {
return this
}
gunit, isGunit := selector.X.(*ast.Ident)
if selector.Sel.Name != "Fixture" || !isGunit || gunit.Name != "gunit" {
return this
}
this.Parent.Validate(this.FixtureName)
return nil
}
77 changes: 77 additions & 0 deletions scan/3_method_finder.go
@@ -0,0 +1,77 @@
package scan

import (
"go/ast"
"strings"
)

type fixtureMethodFinder struct {
fixtures map[string]*fixtureInfo
}

func newFixtureMethodFinder(fixtures map[string]*fixtureInfo) *fixtureMethodFinder {
return &fixtureMethodFinder{fixtures: fixtures}
}

func (this *fixtureMethodFinder) Find(file *ast.File) map[string]*fixtureInfo {
ast.Walk(this, file) // Calls this.Visit(...) recursively.
return this.fixtures
}

func (this *fixtureMethodFinder) Visit(node ast.Node) ast.Visitor {
function, isFunction := node.(*ast.FuncDecl)
if !isFunction {
return this
}

if function.Recv.NumFields() == 0 {
return nil
}

receiver, isPointer := function.Recv.List[0].Type.(*ast.StarExpr)
if !isPointer {
return this
}

fixtureName := receiver.X.(*ast.Ident).Name
fixture, functionMatchesFixture := this.fixtures[fixtureName]
if !functionMatchesFixture {
return nil
}

if !isExportedAndVoidAndNiladic(function) {
return this
}

this.attach(function, fixture)
return nil
}

func isExportedAndVoidAndNiladic(function *ast.FuncDecl) bool {
if isExported := function.Name.IsExported(); !isExported {
return false
}
if isNiladic := function.Type.Params.NumFields() == 0; !isNiladic {
return false
}
isVoid := function.Type.Results.NumFields() == 0
return isVoid
}

func (this *fixtureMethodFinder) attach(function *ast.FuncDecl, fixture *fixtureInfo) {
if IsTestCase(function.Name.Name) {
fixture.TestCases = append(fixture.TestCases, &testCaseInfo{
CharacterPosition: int(function.Pos()),
Name: function.Name.Name,
})
}
}

func IsTestCase(name string) bool {
return strings.HasPrefix(name, "Test") ||
strings.HasPrefix(name, "LongTest") ||
strings.HasPrefix(name, "FocusTest") ||
strings.HasPrefix(name, "FocusLongTest") ||
strings.HasPrefix(name, "SkipTest") ||
strings.HasPrefix(name, "SkipLongTest")
}
13 changes: 13 additions & 0 deletions scan/fixture.go
@@ -0,0 +1,13 @@
package scan

type fixtureInfo struct {
Filename string
StructName string
TestCases []*testCaseInfo
}

type testCaseInfo struct {
CharacterPosition int
LineNumber int
Name string
}

0 comments on commit 8e7b5a5

Please sign in to comment.