forked from maxbrunsfeld/counterfeiter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract argument parsing into a separate package
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
Showing
5 changed files
with
340 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.