diff --git a/.ahoy.yml b/.ahoy.yml index 8189bea..023b663 100644 --- a/.ahoy.yml +++ b/.ahoy.yml @@ -6,14 +6,20 @@ commands: install: cmd: "go install" usage: Build ahoy using go install. + bats: + usage: "Run the bats bash testing command." + cmd: | + bats tests test: usage: Run automated tests cmd: | + ahoy build FAIL=false TESTS=( 'go vet' 'go test -v -race ' 'golint -set_exit_status' + 'bats tests' ) for i in "${TESTS[@]}"; do printf "\n=== TEST: $i ===\n\n" diff --git a/.gitignore b/.gitignore index 4f668d4..23c35a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ # Ignore the items build by sphinx for read the docs. docs/_build +# Ignore the ahoy binary when it's built +ahoy diff --git a/ahoy.go b/ahoy.go index 13d74cc..fe64aed 100644 --- a/ahoy.go +++ b/ahoy.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "github.com/codegangsta/cli" @@ -34,73 +35,85 @@ type Command struct { } var app *cli.App -var sourcedir string var sourcefile string var args []string var verbose bool var bashCompletion bool -var version string - //The build version can be set using the go linker flag `-ldflags "-X main.version=$VERSION"` //Complete command: `go build -ldflags "-X main.version=$VERSION"` +var version string + +// AhoyConf stores the global config. +var AhoyConf struct { + srcDir string + srcFile string +} + func logger(errType string, text string) { errText := "" - if (errType == "error") || (errType == "fatal") || (verbose == true) { - errText = "AHOY! [" + errType + "] ==> " + text + "\n" - log.Print(errText) + // Disable the flags which add date and time for instance. + log.SetFlags(0) + if errType != "debug" { + errText = "[" + errType + "] " + text + "\n" + log.Println(errText) } + if errType == "fatal" { - panic(errText) + os.Exit(1) } } func getConfigPath(sourcefile string) (string, error) { var err error + var config = "" // If a specific source file was set, then try to load it directly. if sourcefile != "" { if _, err := os.Stat(sourcefile); err == nil { return sourcefile, err } - logger("fatal", "An ahoy config file was specified using -f to be at "+sourcefile+" but couldn't be found. Check your path.") + err = errors.New("An ahoy config file was specified using -f to be at " + sourcefile + " but couldn't be found. Check your path.") + return config, err } dir, err := os.Getwd() if err != nil { - log.Fatal(err) + return config, err } for dir != "/" && err == nil { ymlpath := filepath.Join(dir, ".ahoy.yml") //log.Println(ymlpath) if _, err := os.Stat(ymlpath); err == nil { - //log.Println("found: ", ymlpath ) + logger("debug", "Found .ahoy.yml at "+ymlpath) return ymlpath, err } // Chop off the last part of the path. dir = path.Dir(dir) } + logger("debug", "Can't find a .ahoy.yml file.") return "", err } -func getConfig(sourcefile string) (Config, error) { - - yamlFile, err := ioutil.ReadFile(sourcefile) +func getConfig(file string) (Config, error) { + var config = Config{} + yamlFile, err := ioutil.ReadFile(file) if err != nil { - logger("fatal", "An ahoy config file couldn't be found in your path. You can create an example one by using 'ahoy init'.") + err = errors.New("an ahoy config file couldn't be found in your path. You can create an example one by using 'ahoy init'") + return config, err } - var config Config // Extract the yaml file into the config varaible. err = yaml.Unmarshal(yamlFile, &config) if err != nil { - panic(err) + return config, err } // All ahoy files (and imports) must specify the ahoy version. // This is so we can support backwards compatability in the future. if config.AhoyAPI != "v2" { - logger("fatal", "Ahoy only supports API version 'v2', but '"+config.AhoyAPI+"' given in "+sourcefile) + err = errors.New("Ahoy only supports API version 'v2', but '" + config.AhoyAPI + "' given in " + sourcefile) + return config, err } return config, err @@ -117,7 +130,7 @@ func getSubCommands(includes []string) []cli.Command { continue } if include[0] != "/"[0] || include[0] != "~"[0] { - include = filepath.Join(sourcedir, include) + include = filepath.Join(AhoyConf.srcDir, include) } if _, err := os.Stat(include); err != nil { //Skipping files that cannot be loaded allows us to separate @@ -188,13 +201,11 @@ func runCommand(name string, c string) { cReplace := strings.Replace(c, "{{args}}", strings.Join(args, " "), -1) - dir := sourcedir - if verbose { log.Println("===> AHOY", name, "from", sourcefile, ":", cReplace) } cmd := exec.Command("bash", "-c", cReplace) - cmd.Dir = dir + cmd.Dir = AhoyConf.srcDir cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr @@ -238,6 +249,7 @@ func addDefaultCommands(commands []cli.Command) []cli.Command { //TODO Move these to flag.go? func init() { + logger("debug", "init()") flag.StringVar(&sourcefile, "f", "", "specify the sourcefile") flag.BoolVar(&bashCompletion, "generate-bash-completion", false, "") flag.BoolVar(&verbose, "verbose", false, "") @@ -245,6 +257,7 @@ func init() { // BashComplete prints the list of subcommands as the default app completion method func BashComplete(c *cli.Context) { + logger("debug", "BashComplete()") if sourcefile != "" { log.Println(sourcefile) @@ -257,11 +270,56 @@ func BashComplete(c *cli.Context) { } } -func setupApp(args []string) *cli.App { - initFlags(args) - //log.Println(sourcefile) +// NoArgsAction is the application wide default action, for when no flags or arguments +// are passed or when a command doesn't exist. +// Looks like -f flag still works through here though. +func NoArgsAction(c *cli.Context) { + args := c.Args() + if len(args) > 0 { + msg := "Command not found for '" + strings.Join(args, " ") + "'" + logger("fatal", msg) + } + + cli.ShowAppHelp(c) + + if AhoyConf.srcFile == "" { + logger("error", "No .ahoy.yml found. You can use 'ahoy init' to download an example.") + } + + if !c.Bool("help") || !c.Bool("version") { + logger("fatal", "Missing flag or argument.") + } + + // Looks like we never reach here. + fmt.Println("ERROR: NoArg Action ") +} + +// BeforeCommand runs before every command so arguments or flags must be passed +func BeforeCommand(c *cli.Context) error { + args := c.Args() + if c.Bool("version") { + fmt.Println(version) + return errors.New("don't continue with commands") + } + if c.Bool("help") { + if len(args) > 0 { + cli.ShowCommandHelp(c, args.First()) + } else { + cli.ShowAppHelp(c) + } + return errors.New("don't continue with commands") + } + //fmt.Printf("%+v\n", args) + return nil +} + +func setupApp(localArgs []string) *cli.App { + var err error + initFlags(localArgs) // cli stuff app = cli.NewApp() + app.Action = NoArgsAction + app.Before = BeforeCommand app.Name = "ahoy" app.Version = version app.Usage = "Creates a configurable cli app for running commands." @@ -269,9 +327,21 @@ func setupApp(args []string) *cli.App { app.BashComplete = BashComplete overrideFlags(app) - if sourcefile, err := getConfigPath(sourcefile); err == nil { - sourcedir = filepath.Dir(sourcefile) - config, _ := getConfig(sourcefile) + AhoyConf.srcFile, err = getConfigPath(sourcefile) + if err != nil { + logger("fatal", err.Error()) + } else { + AhoyConf.srcDir = filepath.Dir(AhoyConf.srcFile) + // If we don't have a sourcefile, then just supply the default commands. + if AhoyConf.srcFile == "" { + app.Commands = addDefaultCommands(app.Commands) + app.Run(os.Args) + os.Exit(0) + } + config, err := getConfig(AhoyConf.srcFile) + if err != nil { + logger("fatal", err.Error()) + } app.Commands = getCommands(config) app.Commands = addDefaultCommands(app.Commands) if config.Usage != "" { @@ -304,6 +374,7 @@ VERSION: } func main() { + logger("debug", "main()") app = setupApp(os.Args[1:]) app.Run(os.Args) } diff --git a/ahoy_test.go b/ahoy_test.go index 8c0ecb9..b42d9c6 100644 --- a/ahoy_test.go +++ b/ahoy_test.go @@ -40,7 +40,7 @@ func TestGetCommands(t *testing.T) { func TestGetSubCommand(t *testing.T) { // Since we're not running the app directly, sourcedir doesn't get reset, so // we need to reset it ourselves. TODO: Remove these globals somehow. - sourcedir = "" + AhoyConf.srcDir = "" // When empty return empty list of commands. @@ -121,7 +121,7 @@ func TestGetSubCommand(t *testing.T) { }) if len(actual) != 1 { - t.Error("Sourcedir:", sourcedir) + t.Error("Sourcedir:", AhoyConf.srcDir) t.Error("Failed: expect that two commands with the same name get merged into one.", actual) } @@ -238,14 +238,11 @@ func TestGetConfigPath(t *testing.T) { // TODO: Passing directory should return default } -func TestGetConfigPathPanicOnBogusPath(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("getConfigPath did not fail when passed a bogus path.") - } - }() - - getConfigPath("~/bogus/path") +func TestGetConfigPathErrorOnBogusPath(t *testing.T) { + _, err := getConfigPath("~/bogus/path") + if err == nil { + t.Error("getConfigPath did not fail when passed a bogus path.") + } } func appRun(args []string) (string, error) { diff --git a/circle.yml b/circle.yml index dd7e1c0..bc3a296 100644 --- a/circle.yml +++ b/circle.yml @@ -1,5 +1,6 @@ test: pre: + - npm install -g bats # Make sure this build is using GO > 1.6 for golint to work correctly. # On CircleCI, only the Trusty 14.04 build has a newer version. - go get -u github.com/golang/lint/golint diff --git a/flag.go b/flag.go index 24ff734..9bd7158 100644 --- a/flag.go +++ b/flag.go @@ -43,7 +43,7 @@ func initFlags(incomingFlags []string) { // Reset the sourcedir for when we're testing. Otherwise the global state // is preserved between the tests. - sourcedir = "" + AhoyConf.srcDir = "" // Grab the global flags first ourselves so we can customize the yaml file loaded. // Flags are only parsed once, so we need to do this before cli has the chance to? diff --git a/testdata/simple.ahoy.yml b/testdata/simple.ahoy.yml new file mode 100644 index 0000000..2b28927 --- /dev/null +++ b/testdata/simple.ahoy.yml @@ -0,0 +1,4 @@ +ahoyapi: v2 +commands: + echo: + cmd: echo {{args}} diff --git a/tests/.ahoy.yml b/tests/.ahoy.yml new file mode 100644 index 0000000..e69de29 diff --git a/tests/flags.bats b/tests/flags.bats new file mode 100644 index 0000000..89674a3 --- /dev/null +++ b/tests/flags.bats @@ -0,0 +1,12 @@ +#!/usr/bin/env bats + +@test "get the version of ahoy with --version" { + run ./ahoy -f testdata/simple.ahoy.yml --version + [ $status -eq 0 ] + [ $(expr "$output" : "[0-9.]\.[0-9.]\.[0-9.]") -ne 0 ] +} + +@test "get help instead of running a command with --help" { + result="$(./ahoy -f testdata/simple.ahoy.yml --help echo something)" + [ "$result" != "something" ] +} diff --git a/tests/no-ahoy-file.bats b/tests/no-ahoy-file.bats new file mode 100644 index 0000000..60aa2db --- /dev/null +++ b/tests/no-ahoy-file.bats @@ -0,0 +1,26 @@ +#!/usr/bin/env bats + +setup() { + mv .ahoy.yml tmp.ahoy.yml +} + +teardown() { + mv tmp.ahoy.yml .ahoy.yml +} + +@test "run ahoy without a command and without a .ahoy.yml file" { + run ./ahoy + [ $status -eq 1 ] + [ "${lines[-2]}" == "[error] No .ahoy.yml found. You can use 'ahoy init' to download an example." ] + [ "${lines[-1]}" == "[fatal] Missing flag or argument." ] +} + +@test "run an ahoy command without a .ahoy.yml file" { + run ./ahoy something + [ "$output" == "[fatal] Command not found for 'something'" ] +} + +@test "run ahoy init without a .ahoy.yml file" { + run ./ahoy init + [ "${lines[-1]}" == "example.ahoy.yml downloaded to the current directory. You can customize it to suit your needs!" ] +} diff --git a/tests/simple.bats b/tests/simple.bats new file mode 100644 index 0000000..adcab3a --- /dev/null +++ b/tests/simple.bats @@ -0,0 +1,15 @@ +#!/usr/bin/env bats + +@test "display help text and fatal error when no arguments are passed." { + run ./ahoy -f testdata/simple.ahoy.yml + # Should throw an error. + [ $status -ne 0 ] + echo "$output" + [ "${#lines[@]}" -gt 10 ] + [ "${lines[-1]}" == "[fatal] Missing flag or argument." ] +} + +@test "run a simple ahoy command: echo" { + result="$(./ahoy -f testdata/simple.ahoy.yml echo something)" + [ "$result" == "something" ] +}