Skip to content
This repository was archived by the owner on Mar 7, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ type Grapher interface {
}

type defaultGrapher struct {
searcher searcher
collectionGetter collectionGetter
searcher
collectionGetter
}

type searchArgs struct {
Expand All @@ -64,7 +64,7 @@ type searcher interface {
}

type defaultSearcher struct {
pathStringer pathStringer
pathStringer
}

type collectionGetter interface {
Expand Down
27 changes: 23 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import (
)

type torcher struct {
commander commander
commander
}

type commander interface {
Expand All @@ -59,11 +59,19 @@ type validator interface {

type defaultValidator struct{}

type osWrapper interface {
cmdOutput(*exec.Cmd) ([]byte, error)
}

type defaultOSWrapper struct{}

type pprofer interface {
runPprofCommand(args ...string) ([]byte, error)
}

type defaultPprofer struct{}
type defaultPprofer struct {
osWrapper
}

// newTorcher returns a torcher struct with a default commander
func newTorcher() *torcher {
Expand All @@ -76,12 +84,18 @@ func newTorcher() *torcher {
func newCommander() commander {
return &defaultCommander{
validator: new(defaultValidator),
pprofer: new(defaultPprofer),
pprofer: newPprofer(),
grapher: graph.NewGrapher(),
visualizer: visualization.NewVisualizer(),
}
}

func newPprofer() pprofer {
return &defaultPprofer{
osWrapper: new(defaultOSWrapper),
}
}

// main is the entry point of the application
func main() {
t := newTorcher()
Expand Down Expand Up @@ -196,7 +210,7 @@ func (p *defaultPprofer) runPprofCommand(args ...string) ([]byte, error) {
var buf bytes.Buffer
cmd := exec.Command("go", allArgs...)
cmd.Stderr = &buf
out, err := cmd.Output()
out, err := p.osWrapper.cmdOutput(cmd)
if err != nil {
return nil, err
}
Expand All @@ -212,6 +226,11 @@ func (p *defaultPprofer) runPprofCommand(args ...string) ([]byte, error) {
return out, nil
}

// cmdOutput is a tiny wrapper around cmd.Output to enable test mocking
func (w *defaultOSWrapper) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
return cmd.Output()
}

// validateArgument validates a given command line argument with regex. If the
// argument does not match the expected format, this function returns an error.
func (v *defaultValidator) validateArgument(argument, regex, errorMessage string) error {
Expand Down
52 changes: 52 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package main

import (
"errors"
"testing"

"github.com/codegangsta/cli"
Expand Down Expand Up @@ -160,6 +161,57 @@ func TestValidateArgumentPass(t *testing.T) {
})
}

func TestRunPprofCommand(t *testing.T) {
mockOSWrapper := new(mockOSWrapper)
pprofer := defaultPprofer{
osWrapper: mockOSWrapper,
}

mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte("output"), nil).Once()

sampleArgs := []string{"-seconds", "15", "http://localhost:8080"}
out, err := pprofer.runPprofCommand(sampleArgs...)

assert.Equal(t, []byte("output"), out)
assert.NoError(t, err)
mockOSWrapper.AssertExpectations(t)
}

func TestRunPprofCommandUnderlyingError(t *testing.T) {
mockOSWrapper := new(mockOSWrapper)
pprofer := defaultPprofer{
osWrapper: mockOSWrapper,
}

mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return(nil, errors.New("pprof underlying error")).Once()

sampleArgs := []string{"-seconds", "15", "http://localhost:8080"}
out, err := pprofer.runPprofCommand(sampleArgs...)

assert.Equal(t, 0, len(out))
assert.Error(t, err)
mockOSWrapper.AssertExpectations(t)
}

// 'go tool pprof' doesn't exit on errors with nonzero status codes. This test
// ensures that go-torch will detect undrlying errors despite the pprof bug.
// See pprof issue here https://github.com/golang/go/issues/11510
func TestRunPprofCommandHandlePprofErrorBug(t *testing.T) {
mockOSWrapper := new(mockOSWrapper)
pprofer := defaultPprofer{
osWrapper: mockOSWrapper,
}

mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte{}, nil).Once()

sampleArgs := []string{"-seconds", "15", "http://localhost:8080"}
out, err := pprofer.runPprofCommand(sampleArgs...)

assert.Equal(t, 0, len(out))
assert.Error(t, err)
mockOSWrapper.AssertExpectations(t)
}

func TestNewTorcher(t *testing.T) {
assert.NotNil(t, newTorcher())
}
Expand Down
18 changes: 18 additions & 0 deletions mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
package main

import (
"os/exec"

"github.com/codegangsta/cli"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -74,6 +76,22 @@ type mockPprofer struct {
mock.Mock
}

type mockOSWrapper struct {
mock.Mock
}

func (m *mockOSWrapper) cmdOutput(_a0 *exec.Cmd) ([]byte, error) {
ret := m.Called(_a0)

var r0 []byte
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
r1 := ret.Error(1)

return r0, r1
}

func (m *mockPprofer) runPprofCommand(args ...string) ([]byte, error) {
ret := m.Called(args)

Expand Down
31 changes: 30 additions & 1 deletion visualization/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,36 @@

package visualization

import "github.com/stretchr/testify/mock"
import (
"os/exec"

"github.com/stretchr/testify/mock"
)

type mockOSWrapper struct {
mock.Mock
}

func (m *mockOSWrapper) execLookPath(_a0 string) (string, error) {
println(_a0)
ret := m.Called(_a0)

r0 := ret.Get(0).(string)
r1 := ret.Error(1)

return r0, r1
}
func (m *mockOSWrapper) cmdOutput(_a0 *exec.Cmd) ([]byte, error) {
ret := m.Called(_a0)

var r0 []byte
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
r1 := ret.Error(1)

return r0, r1
}

type mockExecutor struct {
mock.Mock
Expand Down
33 changes: 29 additions & 4 deletions visualization/visualization.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,35 @@ type Visualizer interface {
}

type defaultVisualizer struct {
executor executor
executor
}

type osWrapper interface {
execLookPath(string) (string, error)
cmdOutput(*exec.Cmd) ([]byte, error)
}

type defaultOSWrapper struct{}

type executor interface {
createFile(string, []byte) error
runPerlScript(string) ([]byte, error)
}

type defaultExecutor struct{}
type defaultExecutor struct {
osWrapper
}

func newExecutor() executor {
return &defaultExecutor{
osWrapper: new(defaultOSWrapper),
}
}

// NewVisualizer returns a visualizer struct with default fileCreator
func NewVisualizer() Visualizer {
return &defaultVisualizer{
executor: new(defaultExecutor),
executor: newExecutor(),
}
}

Expand Down Expand Up @@ -90,7 +105,7 @@ func (e *defaultExecutor) runPerlScript(graphInput string) ([]byte, error) {
possibilities := []string{"flamegraph.pl", cwd + "/flamegraph.pl", "flame-graph-gen"}
perlScript := ""
for _, path := range possibilities {
perlScript, err = exec.LookPath(path)
perlScript, err = e.osWrapper.execLookPath(path)
// found a valid script
if err == nil {
break
Expand All @@ -101,7 +116,17 @@ func (e *defaultExecutor) runPerlScript(graphInput string) ([]byte, error) {
}
cmd := exec.Command(perlScript, os.Stdin.Name())
cmd.Stdin = strings.NewReader(graphInput)
out, err := e.osWrapper.cmdOutput(cmd)
return out, err
}

// execLookPath is a tiny wrapper around exec.LookPath to enable test mocking
func (w *defaultOSWrapper) execLookPath(path string) (fullPath string, err error) {
return exec.LookPath(path)
}

// cmdOutput is a tiny wrapper around cmd.Output to enable test mocking
func (w *defaultOSWrapper) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
return cmd.Output()
}

Expand Down
42 changes: 42 additions & 0 deletions visualization/visualization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,48 @@ func TestGenerateFlameGraphExecError(t *testing.T) {
mockExecutor.AssertExpectations(t)
}

func TestRunPerlScriptDoesExist(t *testing.T) {
mockOSWrapper := new(mockOSWrapper)
executor := defaultExecutor{
osWrapper: mockOSWrapper,
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err.Error())
}
mockOSWrapper.On("execLookPath", "flamegraph.pl").Return("", errors.New("DNE")).Once()
mockOSWrapper.On("execLookPath", cwd+"/flamegraph.pl").Return("", errors.New("DNE")).Once()
mockOSWrapper.On("execLookPath", "flame-graph-gen").Return("/somepath/flame-graph-gen", nil).Once()

mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte("output"), nil).Once()

out, err := executor.runPerlScript("some graph input")

assert.Equal(t, []byte("output"), out)
assert.NoError(t, err)
mockOSWrapper.AssertExpectations(t)
}

func TestRunPerlScriptDoesNotExist(t *testing.T) {
mockOSWrapper := new(mockOSWrapper)
executor := defaultExecutor{
osWrapper: mockOSWrapper,
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err.Error())
}
mockOSWrapper.On("execLookPath", "flamegraph.pl").Return("", errors.New("DNE")).Once()
mockOSWrapper.On("execLookPath", cwd+"/flamegraph.pl").Return("", errors.New("DNE")).Once()
mockOSWrapper.On("execLookPath", "flame-graph-gen").Return("", errors.New("DNE")).Once()

out, err := executor.runPerlScript("some graph input")

assert.Equal(t, 0, len(out))
assert.Error(t, err)
mockOSWrapper.AssertExpectations(t)
}

// Smoke test the NewVisualizer method
func TestNewVisualizer(t *testing.T) {
assert.NotNil(t, NewVisualizer())
Expand Down