Skip to content

Commit

Permalink
Extract argument parsing into a separate package
Browse files Browse the repository at this point in the history
I was hoping to make main() simpler, more clear and making it
easier to write unit tests for asking for an interface name interactively.
  • Loading branch information
tjarratt authored and Tim Jarratt committed Oct 20, 2015
1 parent a763be9 commit 5f6e774
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 63 deletions.
7 changes: 7 additions & 0 deletions arguments/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package arguments

import "os"

type CurrentWorkingDir func() string
type SymlinkEvaler func(string) (string, error)
type FileStatReader func(string) (os.FileInfo, error)
17 changes: 17 additions & 0 deletions arguments/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package arguments

import "flag"

var (
fakeNameFlag = flag.String(
"fake-name",
"",
"The name of the fake struct",
)

outputPathFlag = flag.String(
"o",
"",
"The file or directory to which the generated fake will be written",
)
)
100 changes: 100 additions & 0 deletions arguments/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package arguments

import (
"path/filepath"
"regexp"
"strings"
)

type ArgumentParser interface {
ParseArguments(...string) ParsedArguments
}

func NewArgumentParser(
failHandler FailHandler,
currentWorkingDir CurrentWorkingDir,
symlinkEvaler SymlinkEvaler,
fileStatReader FileStatReader,
) ArgumentParser {
return argumentParser{
failHandler: failHandler,
currentWorkingDir: currentWorkingDir,
symlinkEvaler: symlinkEvaler,
fileStatReader: fileStatReader,
}
}

func (argParser argumentParser) ParseArguments(args ...string) ParsedArguments {
sourcePackageDir := argParser.getSourceDir(args[0])
fakeImplName := getFakeName(args[1], *fakeNameFlag)
outputPath := argParser.getOutputPath(sourcePackageDir, fakeImplName, *outputPathFlag)

return ParsedArguments{
SourcePackageDir: sourcePackageDir,
OutputPath: outputPath,

InterfaceName: args[1],
FakeImplName: fakeImplName,

PrintToStdOut: len(args) == 3 && args[2] == "-",
}
}

type argumentParser struct {
failHandler FailHandler
currentWorkingDir CurrentWorkingDir
symlinkEvaler SymlinkEvaler
fileStatReader FileStatReader
}

type ParsedArguments struct {
SourcePackageDir string // abs path to the dir containing the interface to fake
OutputPath string // path to write the fake file to

InterfaceName string // the interface to counterfeit
FakeImplName string // the name of the struct implementing the given interface

PrintToStdOut bool
}

func getFakeName(interfaceName, arg string) string {
if arg == "" {
return "Fake" + interfaceName
} else {
return arg
}
}

var camelRegexp = regexp.MustCompile("([a-z])([A-Z])")

func (argParser argumentParser) getOutputPath(sourceDir, fakeName, arg string) string {
if arg == "" {
snakeCaseName := strings.ToLower(camelRegexp.ReplaceAllString(fakeName, "${1}_${2}"))
return filepath.Join(sourceDir, "fakes", snakeCaseName+".go")
} else {
if !filepath.IsAbs(arg) {
arg = filepath.Join(argParser.currentWorkingDir(), arg)
}
return arg
}
}

func (argParser argumentParser) getSourceDir(arg string) string {
if !filepath.IsAbs(arg) {
arg = filepath.Join(argParser.currentWorkingDir(), arg)
}

arg, _ = argParser.symlinkEvaler(arg)
stat, err := argParser.fileStatReader(arg)
if err != nil {
argParser.failHandler("No such file or directory '%s'", arg)
}

if !stat.IsDir() {
return filepath.Dir(arg)
} else {
return arg
}
}

type FailHandler func(string, ...interface{})
200 changes: 200 additions & 0 deletions arguments/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package arguments_test

import (
"errors"
"os"
"path/filepath"
"testing"
"time"

. "github.com/maxbrunsfeld/counterfeiter/arguments"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("parsing arguments", func() {
var subject ArgumentParser
var parsedArgs ParsedArguments
var args []string

var fail FailHandler
var cwd CurrentWorkingDir
var symlinkEvaler SymlinkEvaler
var fileStatReader FileStatReader

JustBeforeEach(func() {
subject = NewArgumentParser(fail, cwd, symlinkEvaler, fileStatReader)
parsedArgs = subject.ParseArguments(args...)
})

BeforeEach(func() {
fail = func(_ string, _ ...interface{}) {}
cwd = func() string {
return "/home/test-user/workspace"
}

symlinkEvaler = func(input string) (string, error) {
return input, nil
}
fileStatReader = func(filename string) (os.FileInfo, error) {
return fakeFileInfo(filename, true), nil
}
})

Describe("when two arguments are provided", func() {
BeforeEach(func() {
args = []string{"some/path", "MySpecialInterface"}
})

It("indicates to not print to stdout", func() {
Expect(parsedArgs.PrintToStdOut).To(BeFalse())
})

It("provides a name for the fake implementing the interface", func() {
Expect(parsedArgs.FakeImplName).To(Equal("FakeMySpecialInterface"))
})

It("treats the second argument as the interface to counterfeit", func() {
Expect(parsedArgs.InterfaceName).To(Equal("MySpecialInterface"))
})

It("snake cases the filename for the output directory", func() {
Expect(parsedArgs.OutputPath).To(Equal(
filepath.Join(
parsedArgs.SourcePackageDir,
"fakes",
"fake_my_special_interface.go",
),
))
})

Describe("the source directory", func() {
It("should be an absolute path", func() {
Expect(filepath.IsAbs(parsedArgs.SourcePackageDir)).To(BeTrue())
})

Context("when the first arg is a path to a file", func() {
BeforeEach(func() {
fileStatReader = func(filename string) (os.FileInfo, error) {
return fakeFileInfo(filename, false), nil
}
})

It("should be the directory containing the file", func() {
Expect(parsedArgs.SourcePackageDir).ToNot(ContainSubstring("something.go"))
})
})

Context("when the file stat cannot be read", func() {
var failWasCalled bool

BeforeEach(func() {
fail = func(_ string, _ ...interface{}) { failWasCalled = true }
fileStatReader = func(_ string) (os.FileInfo, error) {
return fakeFileInfo("", false), errors.New("submarine-shoutout")
}
})

It("should call its fail handler", func() {
Expect(failWasCalled).To(BeTrue())
})
})
})
})

Describe("when three arguments are provided", func() {
Context("and the third one is '-'", func() {
BeforeEach(func() {
args = []string{"some/path", "MySpecialInterface", "-"}
})

It("treats the second argument as the interface to counterfeit", func() {
Expect(parsedArgs.InterfaceName).To(Equal("MySpecialInterface"))
})

It("provides a name for the fake implementing the interface", func() {
Expect(parsedArgs.FakeImplName).To(Equal("FakeMySpecialInterface"))
})

It("indicates that the fake should be printed to stdout", func() {
Expect(parsedArgs.PrintToStdOut).To(BeTrue())
})

It("snake cases the filename for the output directory", func() {
Expect(parsedArgs.OutputPath).To(Equal(
filepath.Join(
parsedArgs.SourcePackageDir,
"fakes",
"fake_my_special_interface.go",
),
))
})

Describe("the source directory", func() {
It("should be an absolute path", func() {
Expect(filepath.IsAbs(parsedArgs.SourcePackageDir)).To(BeTrue())
})

Context("when the first arg is a path to a file", func() {
BeforeEach(func() {
fileStatReader = func(filename string) (os.FileInfo, error) {
return fakeFileInfo(filename, false), nil
}
})

It("should be the directory containing the file", func() {
Expect(parsedArgs.SourcePackageDir).ToNot(ContainSubstring("something.go"))
})
})
})
})

Context("and the third one is some random input", func() {
BeforeEach(func() {
args = []string{"some/path", "MySpecialInterface", "WHOOPS"}
})

It("indicates to not print to stdout", func() {
Expect(parsedArgs.PrintToStdOut).To(BeFalse())
})
})
})
})

func TestCounterfeiterCLI(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Argument Parser Suite")
}

func fakeFileInfo(filename string, isDir bool) os.FileInfo {
return testFileInfo{name: filename, isDir: isDir}
}

type testFileInfo struct {
name string
isDir bool
}

func (testFileInfo testFileInfo) Name() string {
return testFileInfo.name
}

func (testFileInfo testFileInfo) IsDir() bool {
return testFileInfo.isDir
}

func (testFileInfo testFileInfo) Size() int64 {
return 0
}

func (testFileInfo testFileInfo) Mode() os.FileMode {
return 0
}

func (testFileInfo testFileInfo) ModTime() time.Time {
return time.Now()
}

func (testFileInfo testFileInfo) Sys() interface{} {
return nil
}
Loading

0 comments on commit 5f6e774

Please sign in to comment.