From a50193f10c88cf976a1475c80369295475271f98 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 3 Apr 2014 15:51:35 +0100 Subject: [PATCH 01/19] new fresh2 concept with sections of commands in a Freshfile --- _examples/gocraft-web/main.go | 31 -------- _examples/martini/main.go | 27 ------- _examples/pilu-martini/main.go | 13 ---- _examples/traffic/main.go | 15 ---- _test_fixtures/Freshfile | 6 ++ command.go | 68 ++++++++++++++++ command_test.go | 22 ++++++ logger.go | 54 +++++++++++++ main.go | 40 ++++------ runner.conf.sample | 12 --- runner.go | 88 +++++++++++++++++++++ runner/build.go | 39 ---------- runner/logger.go | 40 ---------- runner/runner.go | 39 ---------- runner/runnerutils/utils.go | 80 ------------------- runner/settings.go | 138 --------------------------------- runner/start.go | 127 ------------------------------ runner/utils.go | 63 --------------- runner/utils_test.go | 21 ----- runner/watcher.go | 51 ------------ runner_test.go | 29 +++++++ section.go | 66 ++++++++++++++++ section_test.go | 28 +++++++ 23 files changed, 376 insertions(+), 721 deletions(-) delete mode 100644 _examples/gocraft-web/main.go delete mode 100644 _examples/martini/main.go delete mode 100644 _examples/pilu-martini/main.go delete mode 100644 _examples/traffic/main.go create mode 100644 _test_fixtures/Freshfile create mode 100644 command.go create mode 100644 command_test.go create mode 100644 logger.go delete mode 100644 runner.conf.sample create mode 100644 runner.go delete mode 100644 runner/build.go delete mode 100644 runner/logger.go delete mode 100644 runner/runner.go delete mode 100644 runner/runnerutils/utils.go delete mode 100644 runner/settings.go delete mode 100644 runner/start.go delete mode 100644 runner/utils.go delete mode 100644 runner/utils_test.go delete mode 100644 runner/watcher.go create mode 100644 runner_test.go create mode 100644 section.go create mode 100644 section_test.go diff --git a/_examples/gocraft-web/main.go b/_examples/gocraft-web/main.go deleted file mode 100644 index a549397..0000000 --- a/_examples/gocraft-web/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "github.com/gocraft/web" - "github.com/pilu/fresh/runner/runnerutils" - "net/http" -) - -func runnerMiddleware(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - if runnerutils.HasErrors() { - runnerutils.RenderError(rw) - return - } - - next(rw, req) -} - -type Context struct{} - -func (c *Context) SayHello(rw web.ResponseWriter, req *web.Request) { - fmt.Fprint(rw, "Hello World") -} - -func main() { - router := web.New(Context{}). - Middleware(web.LoggerMiddleware). - Middleware(runnerMiddleware). - Get("/", (*Context).SayHello) - http.ListenAndServe("localhost:3000", router) -} diff --git a/_examples/martini/main.go b/_examples/martini/main.go deleted file mode 100644 index 9cd80ec..0000000 --- a/_examples/martini/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "github.com/codegangsta/martini" - "github.com/pilu/fresh/runner/runnerutils" - "net/http" - "os" -) - -func runnerMiddleware(w http.ResponseWriter, r *http.Request) { - if runnerutils.HasErrors() { - runnerutils.RenderError(w) - } -} - -func main() { - m := martini.Classic() - - if os.Getenv("DEV_RUNNER") == "1" { - m.Use(runnerMiddleware) - } - - m.Get("/", func() string { - return "Hello world - Martini" - }) - m.Run() -} diff --git a/_examples/pilu-martini/main.go b/_examples/pilu-martini/main.go deleted file mode 100644 index 9846b75..0000000 --- a/_examples/pilu-martini/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "github.com/pilu/martini" -) - -func main() { - m := martini.Classic() - m.Get("/", func() string { - return "Hello world - Martini" - }) - m.Run() -} diff --git a/_examples/traffic/main.go b/_examples/traffic/main.go deleted file mode 100644 index 7ce40af..0000000 --- a/_examples/traffic/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "github.com/pilu/traffic" -) - -func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { - w.WriteText("Hello World - Traffic") -} - -func main() { - router := traffic.New() - router.Get("/", rootHandler) - router.Run() -} diff --git a/_test_fixtures/Freshfile b/_test_fixtures/Freshfile new file mode 100644 index 0000000..c0f0f79 --- /dev/null +++ b/_test_fixtures/Freshfile @@ -0,0 +1,6 @@ +[.go, .tpl, .tmpl, .html] +test: go test +build: go build -o hello + +[stylesheets: .less] +less-compiler: lessc less/application.less > css/apllication.css diff --git a/command.go b/command.go new file mode 100644 index 0000000..2586b50 --- /dev/null +++ b/command.go @@ -0,0 +1,68 @@ +package main + +import ( + "io" + "log" + "os/exec" + "strings" +) + +type Command struct { + Name string + CmdString string + Cmd *exec.Cmd + Stdout io.ReadCloser + Stderr io.ReadCloser + Logger *Logger +} + +func newCommand(name, command string) *Command { + c := &Command{ + Name: name, + CmdString: command, + Logger: newLogger(name), + } + + return c +} + +func (c *Command) build() error { + options := strings.Split(c.CmdString, " ") + c.Cmd = exec.Command(options[0], options[1:]...) + + var err error + c.Stdout, err = c.Cmd.StdoutPipe() + if err != nil { + return err + } + + c.Stderr, err = c.Cmd.StderrPipe() + if err != nil { + return err + } + + return nil +} + +func (c *Command) Run(done chan bool) { + logger.log("running command %v\n", c.Name) + + err := c.build() + if err != nil { + log.Fatal(err) + } + + go io.Copy(c.Logger, c.Stdout) + go io.Copy(c.Logger, c.Stderr) + + err = c.Cmd.Run() + logger.log("Errors on %s: %v\n", c.CmdString, err) + done <- true +} + +func (c *Command) Stop() { + if c.Cmd != nil && c.Cmd.Process != nil { + logger.log("killing process %v\n", c.CmdString) + c.Cmd.Process.Kill() + } +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..a923f4d --- /dev/null +++ b/command_test.go @@ -0,0 +1,22 @@ +package main + +import ( + assert "github.com/pilu/miniassert" + "testing" +) + +func TestNewCommand(t *testing.T) { + c := newCommand("build", "./build all -o foo") + assert.Equal(t, "build", c.Name) + assert.Equal(t, "./build all -o foo", c.CmdString) +} + +func TestCommand_Build(t *testing.T) { + c := newCommand("build", "./build all -o foo") + assert.Nil(t, c.Cmd) + + c.build() + assert.NotNil(t, c.Cmd) + assert.Equal(t, "./build", c.Cmd.Path) + assert.Equal(t, []string{"./build", "all", "-o", "foo"}, c.Cmd.Args) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..42f2e5d --- /dev/null +++ b/logger.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" +) + +type Logger struct { + Name string + Verbose bool + *log.Logger +} + +func (logger *Logger) Write(p []byte) (n int, err error) { + logger.log(string(p)) + return len(p), nil +} + +var ( + loggersCount int + loggerMaxNameLength int +) + +func newLogger(name string) *Logger { + _logger := log.New(os.Stderr, "", 0) + + logger := &Logger{ + Name: name, + Logger: _logger, + Verbose: true, + } + + loggersCount++ + if length := len(name); length > loggerMaxNameLength { + loggerMaxNameLength = length + } + + return logger +} + +func (logger *Logger) log(format string, v ...interface{}) { + if !logger.Verbose { + return + } + now := time.Now() + timeString := fmt.Sprintf("%d:%d:%02d", now.Hour(), now.Minute(), now.Second()) + // format = fmt.Sprintf("%s%s %s |%s %s", color, timeString, prefix, clear, format) + // logger.Logger.Printf(format, v...) + prefix := fmt.Sprintf("%s %-10s |", timeString, logger.Name) + format = fmt.Sprintf("%s %s", prefix, format) + logger.Logger.Printf(format, v...) +} diff --git a/main.go b/main.go index 1dea9dd..3dc777a 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,26 @@ -/* -Fresh is a command line tool that builds and (re)starts your web application everytime you save a go or template file. - -If the web framework you are using supports the Fresh runner, it will show build errors on your browser. - -It currently works with Traffic (https://github.com/pilu/traffic), Martini (https://github.com/codegangsta/martini) and gocraft/web (https://github.com/gocraft/web). - -Fresh will watch for file events, and every time you create/modifiy/delete a file it will build and restart the application. -If `go build` returns an error, it will logs it in the tmp folder. - -Traffic (https://github.com/pilu/traffic) already has a middleware that shows the content of that file if it is present. This middleware is automatically added if you run a Traffic web app in dev mode with Fresh. -*/ package main import ( - "flag" - "fmt" - "github.com/pilu/fresh/runner" "os" + "flag" ) -func main() { - configPath := flag.String("c", "", "config file path") +var logger *Logger + +func init() { + logger = newLogger("fresh") + flag.BoolVar(&logger.Verbose, "verbose", false, "verbose") flag.Parse() +} - if *configPath != "" { - if _, err := os.Stat(*configPath); err != nil { - fmt.Printf("Can't find config file `%s`\n", *configPath) - os.Exit(1) - } else { - os.Setenv("RUNNER_CONFIG_PATH", *configPath) - } +func main() { + r, err := newRunnerWithFreshfile("Freshfile") + if err != nil { + logger.log("%s\n", err.Error()) + os.Exit(1) } - runner.Start() + done := make(chan bool) + r.Run(done) + <-r.DoneChan } diff --git a/runner.conf.sample b/runner.conf.sample deleted file mode 100644 index 5ac3c05..0000000 --- a/runner.conf.sample +++ /dev/null @@ -1,12 +0,0 @@ -root: . -tmp_path: ./tmp -build_name: runner-build -build_log: runner-build-errors.log -valid_ext: .go, .tpl, .tmpl, .html -build_delay: 600 -colors: 1 -log_color_main: cyan -log_color_build: yellow -log_color_runner: green -log_color_watcher: magenta -log_color_app: diff --git a/runner.go b/runner.go new file mode 100644 index 0000000..bb09bee --- /dev/null +++ b/runner.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "github.com/pilu/config" + "os" + "os/signal" + "runtime" + "sync" + "time" +) + +type Runner struct { + Sections []*Section + DoneChan chan bool + StopChan chan bool +} + +func newRunner() *Runner { + return &Runner{ + StopChan: make(chan bool), + } +} + +func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { + r := newRunner() + + sections, err := config.ParseFile(freshfilePath, "main: *") + if err != nil { + return r, err + } + + for s, opts := range sections { + section := r.NewSection(s) + for name, cmd := range opts { + section.NewCommand(name, cmd) + } + } + + return r, nil +} + +func (r *Runner) NewSection(description string) *Section { + s := newSection(description, r.StopChan) + r.Sections = append(r.Sections, s) + return s +} + +func (r *Runner) Run(done chan bool) { + var wg sync.WaitGroup + r.Stop() + logger.log("Running...") + logger.log("%d goroutines", runtime.NumGoroutine()) + go r.ListenForSignals(done) + + for _, s := range r.Sections { + wg.Add(1) + go func(s *Section) { + defer wg.Done() + s.Run() + }(s) + } + wg.Wait() + logger.log("finish") +} + +func (r *Runner) Stop() { + logger.log("stopping all sections") + for _, s := range r.Sections { + s.Stop() + } +} + +func (r *Runner) ListenForSignals(done chan bool) { + sc := make(chan os.Signal, 1) + signal.Notify(sc, os.Interrupt) + <-sc + fmt.Printf("Interrupt a second time to quit\n") + select { + case <-sc: + r.StopChan <- true + done <- true + case <-time.After(1 * time.Second): + r.StopChan <- true + r.Stop() + r.Run(done) + } +} diff --git a/runner/build.go b/runner/build.go deleted file mode 100644 index 7f0242f..0000000 --- a/runner/build.go +++ /dev/null @@ -1,39 +0,0 @@ -package runner - -import ( - "io" - "io/ioutil" - "os" - "os/exec" -) - -func build() (string, bool) { - buildLog("Building...") - - cmd := exec.Command("go", "build", "-o", buildPath(), root()) - - stderr, err := cmd.StderrPipe() - if err != nil { - fatal(err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - fatal(err) - } - - err = cmd.Start() - if err != nil { - fatal(err) - } - - io.Copy(os.Stdout, stdout) - errBuf, _ := ioutil.ReadAll(stderr) - - err = cmd.Wait() - if err != nil { - return string(errBuf), false - } - - return "", true -} diff --git a/runner/logger.go b/runner/logger.go deleted file mode 100644 index 4ccc0c3..0000000 --- a/runner/logger.go +++ /dev/null @@ -1,40 +0,0 @@ -package runner - -import ( - "fmt" - logPkg "log" - "os" - "time" -) - -type logFunc func(string, ...interface{}) - -var logger = logPkg.New(os.Stderr, "", 0) - -func newLogFunc(prefix string) func(string, ...interface{}) { - color, clear := "", "" - if settings["colors"] == "1" { - color = fmt.Sprintf("\033[%sm", logColor(prefix)) - clear = fmt.Sprintf("\033[%sm", colors["reset"]) - } - prefix = fmt.Sprintf("%-11s", prefix) - - return func(format string, v ...interface{}) { - now := time.Now() - timeString := fmt.Sprintf("%d:%d:%02d", now.Hour(), now.Minute(), now.Second()) - format = fmt.Sprintf("%s%s %s |%s %s", color, timeString, prefix, clear, format) - logger.Printf(format, v...) - } -} - -func fatal(err error) { - logger.Fatal(err) -} - -type appLogWriter struct{} - -func (a appLogWriter) Write(p []byte) (n int, err error) { - appLog(string(p)) - - return len(p), nil -} diff --git a/runner/runner.go b/runner/runner.go deleted file mode 100644 index f15f89a..0000000 --- a/runner/runner.go +++ /dev/null @@ -1,39 +0,0 @@ -package runner - -import ( - "io" - "os/exec" -) - -func run() bool { - runnerLog("Running...") - - cmd := exec.Command(buildPath()) - - stderr, err := cmd.StderrPipe() - if err != nil { - fatal(err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - fatal(err) - } - - err = cmd.Start() - if err != nil { - fatal(err) - } - - go io.Copy(appLogWriter{}, stderr) - go io.Copy(appLogWriter{}, stdout) - - go func() { - <-stopChannel - pid := cmd.Process.Pid - runnerLog("Killing PID %d", pid) - cmd.Process.Kill() - }() - - return true -} diff --git a/runner/runnerutils/utils.go b/runner/runnerutils/utils.go deleted file mode 100644 index 3c05c1b..0000000 --- a/runner/runnerutils/utils.go +++ /dev/null @@ -1,80 +0,0 @@ -package runnerutils - -import ( - "bufio" - "html/template" - "io/ioutil" - "net/http" - "os" - "path/filepath" -) - -var logFilePath string - -func init() { - root := os.Getenv("RUNNER_WD") - tmpPath := os.Getenv("RUNNER_TMP_PATH") - fileName := os.Getenv("RUNNER_BUILD_LOG") - logFilePath = filepath.Join(root, tmpPath, fileName) -} - -// Returns true if a build error file exists in the tmp folder. -func HasErrors() bool { - if _, err := os.Stat(logFilePath); err == nil { - return true - } - - return false -} - -// It renders an error page with the build error message. -func RenderError(w http.ResponseWriter) { - data := map[string]interface{}{ - "Output": readErrorFile(), - } - - w.Header().Set("Content-Type", "text/html") - tpl := template.Must(template.New("ErrorPage").Parse(buildPageTpl)) - tpl.Execute(w, data) -} - -func readErrorFile() string { - file, err := os.Open(logFilePath) - if err != nil { - return "" - } - - defer file.Close() - - reader := bufio.NewReader(file) - bytes, _ := ioutil.ReadAll(reader) - - return string(bytes) -} - -const buildPageTpl string = ` - - - Traffic Panic - - - - -
-
-

Build Error

-
-
- -
-
{{ .Output }}
-
- - - ` diff --git a/runner/settings.go b/runner/settings.go deleted file mode 100644 index 61e2412..0000000 --- a/runner/settings.go +++ /dev/null @@ -1,138 +0,0 @@ -package runner - -import ( - "fmt" - "github.com/pilu/config" - "os" - "path/filepath" - "strconv" - "strings" - "time" -) - -const ( - envSettingsPrefix = "RUNNER_" - mainSettingsSection = "Settings" -) - -var settings = map[string]string{ - "config_path": "./runner.conf", - "root": ".", - "tmp_path": "./tmp", - "build_name": "runner-build", - "build_log": "runner-build-errors.log", - "valid_ext": ".go, .tpl, .tmpl, .html", - "build_delay": "600", - "colors": "1", - "log_color_main": "cyan", - "log_color_build": "yellow", - "log_color_runner": "green", - "log_color_watcher": "magenta", - "log_color_app": "", -} - -var colors = map[string]string{ - "reset": "0", - "black": "30", - "red": "31", - "green": "32", - "yellow": "33", - "blue": "34", - "magenta": "35", - "cyan": "36", - "white": "37", - "bold_black": "30;1", - "bold_red": "31;1", - "bold_green": "32;1", - "bold_yellow": "33;1", - "bold_blue": "34;1", - "bold_magenta": "35;1", - "bold_cyan": "36;1", - "bold_white": "37;1", - "bright_black": "30;2", - "bright_red": "31;2", - "bright_green": "32;2", - "bright_yellow": "33;2", - "bright_blue": "34;2", - "bright_magenta": "35;2", - "bright_cyan": "36;2", - "bright_white": "37;2", -} - -func logColor(logName string) string { - settingsKey := fmt.Sprintf("log_color_%s", logName) - colorName := settings[settingsKey] - - return colors[colorName] -} - -func loadEnvSettings() { - for key, _ := range settings { - envKey := fmt.Sprintf("%s%s", envSettingsPrefix, strings.ToUpper(key)) - if value := os.Getenv(envKey); value != "" { - settings[key] = value - } - } -} - -func loadRunnerConfigSettings() { - if _, err := os.Stat(configPath()); err != nil { - return - } - - logger.Printf("Loading settings from %s", configPath()) - sections, err := config.ParseFile(configPath(), mainSettingsSection) - if err != nil { - return - } - - for key, value := range sections[mainSettingsSection] { - settings[key] = value - } -} - -func initSettings() { - loadEnvSettings() - loadRunnerConfigSettings() -} - -func getenv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - - return defaultValue -} - -func root() string { - return settings["root"] -} - -func tmpPath() string { - return settings["tmp_path"] -} - -func buildName() string { - return settings["build_name"] -} -func buildPath() string { - return filepath.Join(tmpPath(), buildName()) -} - -func buildErrorsFileName() string { - return settings["build_log"] -} - -func buildErrorsFilePath() string { - return filepath.Join(tmpPath(), buildErrorsFileName()) -} - -func configPath() string { - return settings["config_path"] -} - -func buildDelay() time.Duration { - value, _ := strconv.Atoi(settings["build_delay"]) - - return time.Duration(value) -} diff --git a/runner/start.go b/runner/start.go deleted file mode 100644 index b382dc3..0000000 --- a/runner/start.go +++ /dev/null @@ -1,127 +0,0 @@ -package runner - -import ( - "fmt" - "os" - "runtime" - "strings" - "syscall" - "time" -) - -var ( - startChannel chan string - stopChannel chan bool - mainLog logFunc - watcherLog logFunc - runnerLog logFunc - buildLog logFunc - appLog logFunc -) - -func flushEvents() { - for { - select { - case eventName := <-startChannel: - mainLog("receiving event %s", eventName) - default: - return - } - } -} - -func start() { - loopIndex := 0 - buildDelay := buildDelay() - - started := false - - go func() { - for { - loopIndex++ - mainLog("Waiting (loop %d)...", loopIndex) - eventName := <-startChannel - - mainLog("receiving first event %s", eventName) - mainLog("sleeping for %d milliseconds", buildDelay) - time.Sleep(buildDelay * time.Millisecond) - mainLog("flushing events") - - flushEvents() - - mainLog("Started! (%d Goroutines)", runtime.NumGoroutine()) - err := removeBuildErrorsLog() - if err != nil { - mainLog(err.Error()) - } - - errorMessage, ok := build() - if !ok { - mainLog("Build Failed: \n %s", errorMessage) - if !started { - os.Exit(1) - } - createBuildErrorsLog(errorMessage) - } else { - if started { - stopChannel <- true - } - run() - } - - started = true - mainLog(strings.Repeat("-", 20)) - } - }() -} - -func init() { - startChannel = make(chan string, 1000) - stopChannel = make(chan bool) -} - -func initLogFuncs() { - mainLog = newLogFunc("main") - watcherLog = newLogFunc("watcher") - runnerLog = newLogFunc("runner") - buildLog = newLogFunc("build") - appLog = newLogFunc("app") -} - -func initLimit() { - var rLimit syscall.Rlimit - rLimit.Max = 10000 - rLimit.Cur = 10000 - err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - fmt.Println("Error Setting Rlimit ", err) - } -} - -func setEnvVars() { - os.Setenv("DEV_RUNNER", "1") - wd, err := os.Getwd() - if err == nil { - os.Setenv("RUNNER_WD", wd) - } - - for k, v := range settings { - key := strings.ToUpper(fmt.Sprintf("%s%s", envSettingsPrefix, k)) - os.Setenv(key, v) - } -} - -// Watches for file changes in the root directory. -// After each file system event it builds and (re)starts the application. -func Start() { - initLimit() - initSettings() - initLogFuncs() - initFolders() - setEnvVars() - watch() - start() - startChannel <- "/" - - <-make(chan int) -} diff --git a/runner/utils.go b/runner/utils.go deleted file mode 100644 index 93096ff..0000000 --- a/runner/utils.go +++ /dev/null @@ -1,63 +0,0 @@ -package runner - -import ( - "os" - "path/filepath" - "strings" -) - -func initFolders() { - runnerLog("InitFolders") - path := tmpPath() - runnerLog("mkdir %s", path) - err := os.Mkdir(path, 0755) - if err != nil { - runnerLog(err.Error()) - } -} - -func isTmpDir(path string) bool { - absolutePath, _ := filepath.Abs(path) - absoluteTmpPath, _ := filepath.Abs(tmpPath()) - - return absolutePath == absoluteTmpPath -} - -func isWatchedFile(path string) bool { - absolutePath, _ := filepath.Abs(path) - absoluteTmpPath, _ := filepath.Abs(tmpPath()) - - if strings.HasPrefix(absolutePath, absoluteTmpPath) { - return false - } - - ext := filepath.Ext(path) - - for _, e := range strings.Split(settings["valid_ext"], ",") { - if strings.TrimSpace(e) == ext { - return true - } - } - - return false -} - -func createBuildErrorsLog(message string) bool { - file, err := os.Create(buildErrorsFilePath()) - if err != nil { - return false - } - - _, err = file.WriteString(message) - if err != nil { - return false - } - - return true -} - -func removeBuildErrorsLog() error { - err := os.Remove(buildErrorsFilePath()) - - return err -} diff --git a/runner/utils_test.go b/runner/utils_test.go deleted file mode 100644 index d759a20..0000000 --- a/runner/utils_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package runner - -import ( - assert "github.com/pilu/miniassert" - "testing" -) - -func TestIsWatchedFile(t *testing.T) { - // valid extensions - assert.True(t, isWatchedFile("test.go")) - assert.True(t, isWatchedFile("test.tpl")) - assert.True(t, isWatchedFile("test.tmpl")) - assert.True(t, isWatchedFile("test.html")) - - /* // invalid extensions */ - assert.False(t, isWatchedFile("test.css")) - assert.False(t, isWatchedFile("test-executable")) - - // files in tmp - assert.False(t, isWatchedFile("./tmp/test.go")) -} diff --git a/runner/watcher.go b/runner/watcher.go deleted file mode 100644 index 7df2706..0000000 --- a/runner/watcher.go +++ /dev/null @@ -1,51 +0,0 @@ -package runner - -import ( - "github.com/howeyc/fsnotify" - "os" - "path/filepath" - "strings" -) - -func watchFolder(path string) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - fatal(err) - } - - go func() { - for { - select { - case ev := <-watcher.Event: - if isWatchedFile(ev.Name) { - watcherLog("sending event %s", ev) - startChannel <- ev.String() - } - case err := <-watcher.Error: - watcherLog("error: %s", err) - } - } - }() - - watcherLog("Watching %s", path) - err = watcher.Watch(path) - - if err != nil { - fatal(err) - } -} - -func watch() { - root := root() - filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if info.IsDir() && !isTmpDir(path) { - if len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") { - return filepath.SkipDir - } - - watchFolder(path) - } - - return err - }) -} diff --git a/runner_test.go b/runner_test.go new file mode 100644 index 0000000..02d2e64 --- /dev/null +++ b/runner_test.go @@ -0,0 +1,29 @@ +package main + +import ( + assert "github.com/pilu/miniassert" + "testing" +) + +func TestNewRunner(t *testing.T) { + r := newRunner() + assert.Equal(t, 0, len(r.Sections)) +} + +func TestRunner_NewSection(t *testing.T) { + r := newRunner() + s := r.NewSection(".go") + assert.Equal(t, 1, len(r.Sections)) + assert.Equal(t, s, r.Sections[0]) +} + +func TestNewRunnerWithFreshfile_WithFileNotFound(t *testing.T) { + _, err := newRunnerWithFreshfile("./_test_fixtures/file-not-found") + assert.NotNil(t, err) +} + +func TestNewRunnerWithFreshfile_WithValidFile(t *testing.T) { + r, err := newRunnerWithFreshfile("./_test_fixtures/Freshfile") + assert.Nil(t, err) + assert.Equal(t, 3, len(r.Sections)) +} diff --git a/section.go b/section.go new file mode 100644 index 0000000..472881b --- /dev/null +++ b/section.go @@ -0,0 +1,66 @@ +package main + +import ( + "regexp" + "strings" +) + +type Section struct { + Name string + Extensions []string + Commands []*Command + StopChan chan bool +} + +func newSection(description string, stopChan chan bool) *Section { + var name string + parts := strings.Split(description, ":") + if len(parts) > 1 { + name = parts[0] + description = parts[1] + } + + extRe := regexp.MustCompile(`\s*,\s*`) + extensions := []string{} + for _, rawExtension := range extRe.Split(description, -1) { + extension := strings.TrimSpace(rawExtension) + if len(extension) > 0 { + extensions = append(extensions, extension) + } + } + + return &Section{ + Name: name, + Extensions: extensions, + StopChan: stopChan, + } +} + +func (s *Section) NewCommand(name, command string) *Command { + c := newCommand(name, command) + s.Commands = append(s.Commands, c) + return c +} + +func (s *Section) Run() { + logger.log("Running section %s", s.Name) + for _, c := range s.Commands { + done := make(chan bool) + go func(c *Command) { + c.Run(done) + }(c) + select { + case <-done: + continue + case <-s.StopChan: + return + } + } +} + +func (s *Section) Stop() { + logger.log("Stopping section %s", s.Name) + for _, c := range s.Commands { + c.Stop() + } +} diff --git a/section_test.go b/section_test.go new file mode 100644 index 0000000..29b00d4 --- /dev/null +++ b/section_test.go @@ -0,0 +1,28 @@ +package main + +import ( + assert "github.com/pilu/miniassert" + "testing" +) + +func TestNewSection(t *testing.T) { + var s *Section + + s = newSection(".go,.tpl, .tmpl, .html, , , ", make(chan bool)) + assert.Equal(t, "", s.Name) + assert.Equal(t, []string{".go", ".tpl", ".tmpl", ".html"}, s.Extensions) + assert.Equal(t, 0, len(s.Commands)) + + s = newSection("stylesheets: .css, .less", make(chan bool)) + assert.Equal(t, "stylesheets", s.Name) + assert.Equal(t, []string{".css", ".less"}, s.Extensions) + assert.Equal(t, 0, len(s.Commands)) +} + +func TestSection_NewCommand(t *testing.T) { + s := newSection("go", make(chan bool)) + assert.Equal(t, 0, len(s.Commands)) + c := s.NewCommand("build", "./build") + assert.Equal(t, 1, len(s.Commands)) + assert.Equal(t, c, s.Commands[0]) +} From b3fc66123c68c42e2f052f89e7b8fc991441673c Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Mon, 14 Apr 2014 14:35:23 +0100 Subject: [PATCH 02/19] stops commands if one fails --- command.go | 13 ++++++++----- main.go | 6 ++++-- runner.go | 29 ++++++++++++----------------- section.go | 19 ++++++------------- 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/command.go b/command.go index 2586b50..71e8763 100644 --- a/command.go +++ b/command.go @@ -44,8 +44,8 @@ func (c *Command) build() error { return nil } -func (c *Command) Run(done chan bool) { - logger.log("running command %v\n", c.Name) +func (c *Command) Run() error { + logger.log("Running command %v\n", c.Name) err := c.build() if err != nil { @@ -56,13 +56,16 @@ func (c *Command) Run(done chan bool) { go io.Copy(c.Logger, c.Stderr) err = c.Cmd.Run() - logger.log("Errors on %s: %v\n", c.CmdString, err) - done <- true + if err != nil { + logger.log("Errors on `%s`: %v\n", c.Name, err) + } + + return err } func (c *Command) Stop() { if c.Cmd != nil && c.Cmd.Process != nil { - logger.log("killing process %v\n", c.CmdString) + logger.log("Killing process `%s`\n", c.Name) c.Cmd.Process.Kill() } } diff --git a/main.go b/main.go index 3dc777a..b77c0db 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "os" "flag" + "os" ) var logger *Logger @@ -20,7 +20,9 @@ func main() { os.Exit(1) } + logger.log("Process %d", os.Getpid()) done := make(chan bool) r.Run(done) - <-r.DoneChan + <-done + println("the end") } diff --git a/runner.go b/runner.go index bb09bee..5b7cde7 100644 --- a/runner.go +++ b/runner.go @@ -2,24 +2,20 @@ package main import ( "fmt" - "github.com/pilu/config" "os" "os/signal" "runtime" - "sync" "time" + + "github.com/pilu/config" ) type Runner struct { Sections []*Section - DoneChan chan bool - StopChan chan bool } func newRunner() *Runner { - return &Runner{ - StopChan: make(chan bool), - } + return &Runner{} } func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { @@ -41,48 +37,47 @@ func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { } func (r *Runner) NewSection(description string) *Section { - s := newSection(description, r.StopChan) + s := newSection(description) r.Sections = append(r.Sections, s) return s } func (r *Runner) Run(done chan bool) { - var wg sync.WaitGroup - r.Stop() logger.log("Running...") logger.log("%d goroutines", runtime.NumGoroutine()) go r.ListenForSignals(done) for _, s := range r.Sections { - wg.Add(1) go func(s *Section) { - defer wg.Done() s.Run() }(s) } - wg.Wait() - logger.log("finish") } func (r *Runner) Stop() { - logger.log("stopping all sections") + logger.log("Stopping all sections") for _, s := range r.Sections { s.Stop() } } func (r *Runner) ListenForSignals(done chan bool) { + logger.log("Listening for signals") sc := make(chan os.Signal, 1) signal.Notify(sc, os.Interrupt) + signal.Notify(sc, os.Kill) <-sc fmt.Printf("Interrupt a second time to quit\n") + logger.log("Waiting for a second signal") select { case <-sc: - r.StopChan <- true + logger.log("Second signal received") done <- true case <-time.After(1 * time.Second): - r.StopChan <- true + logger.log("Timeout") + logger.log("Stopping...") r.Stop() + logger.log("Calling Run...") r.Run(done) } } diff --git a/section.go b/section.go index 472881b..82cb6d5 100644 --- a/section.go +++ b/section.go @@ -9,10 +9,9 @@ type Section struct { Name string Extensions []string Commands []*Command - StopChan chan bool } -func newSection(description string, stopChan chan bool) *Section { +func newSection(description string) *Section { var name string parts := strings.Split(description, ":") if len(parts) > 1 { @@ -32,7 +31,6 @@ func newSection(description string, stopChan chan bool) *Section { return &Section{ Name: name, Extensions: extensions, - StopChan: stopChan, } } @@ -43,19 +41,14 @@ func (s *Section) NewCommand(name, command string) *Command { } func (s *Section) Run() { - logger.log("Running section %s", s.Name) + logger.log("Running section `%s`", s.Name) for _, c := range s.Commands { - done := make(chan bool) - go func(c *Command) { - c.Run(done) - }(c) - select { - case <-done: - continue - case <-s.StopChan: - return + err := c.Run() + if err != nil { + break } } + logger.log("Section ended `%s`", s.Name) } func (s *Section) Stop() { From 314f882b057a5dc021d050432927d614922994c9 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Mon, 14 Apr 2014 15:07:46 +0100 Subject: [PATCH 03/19] added Done channel to Runner --- main.go | 5 ++--- runner.go | 15 +++++++++------ section_test.go | 9 +++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index b77c0db..28ea600 100644 --- a/main.go +++ b/main.go @@ -21,8 +21,7 @@ func main() { } logger.log("Process %d", os.Getpid()) - done := make(chan bool) - r.Run(done) - <-done + r.Run() + <-r.DoneChan println("the end") } diff --git a/runner.go b/runner.go index 5b7cde7..36ecba3 100644 --- a/runner.go +++ b/runner.go @@ -12,10 +12,13 @@ import ( type Runner struct { Sections []*Section + DoneChan chan bool } func newRunner() *Runner { - return &Runner{} + return &Runner{ + DoneChan: make(chan bool), + } } func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { @@ -42,10 +45,10 @@ func (r *Runner) NewSection(description string) *Section { return s } -func (r *Runner) Run(done chan bool) { +func (r *Runner) Run() { logger.log("Running...") logger.log("%d goroutines", runtime.NumGoroutine()) - go r.ListenForSignals(done) + go r.ListenForSignals() for _, s := range r.Sections { go func(s *Section) { @@ -61,7 +64,7 @@ func (r *Runner) Stop() { } } -func (r *Runner) ListenForSignals(done chan bool) { +func (r *Runner) ListenForSignals() { logger.log("Listening for signals") sc := make(chan os.Signal, 1) signal.Notify(sc, os.Interrupt) @@ -72,12 +75,12 @@ func (r *Runner) ListenForSignals(done chan bool) { select { case <-sc: logger.log("Second signal received") - done <- true + r.DoneChan <- true case <-time.After(1 * time.Second): logger.log("Timeout") logger.log("Stopping...") r.Stop() logger.log("Calling Run...") - r.Run(done) + r.Run() } } diff --git a/section_test.go b/section_test.go index 29b00d4..75cd8e4 100644 --- a/section_test.go +++ b/section_test.go @@ -1,26 +1,27 @@ package main import ( - assert "github.com/pilu/miniassert" "testing" + + assert "github.com/pilu/miniassert" ) func TestNewSection(t *testing.T) { var s *Section - s = newSection(".go,.tpl, .tmpl, .html, , , ", make(chan bool)) + s = newSection(".go,.tpl, .tmpl, .html, , , ") assert.Equal(t, "", s.Name) assert.Equal(t, []string{".go", ".tpl", ".tmpl", ".html"}, s.Extensions) assert.Equal(t, 0, len(s.Commands)) - s = newSection("stylesheets: .css, .less", make(chan bool)) + s = newSection("stylesheets: .css, .less") assert.Equal(t, "stylesheets", s.Name) assert.Equal(t, []string{".css", ".less"}, s.Extensions) assert.Equal(t, 0, len(s.Commands)) } func TestSection_NewCommand(t *testing.T) { - s := newSection("go", make(chan bool)) + s := newSection("go") assert.Equal(t, 0, len(s.Commands)) c := s.NewCommand("build", "./build") assert.Equal(t, 1, len(s.Commands)) From fa2b41a6eba27e0418894c572334d717b2a80ba4 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Mon, 14 Apr 2014 15:11:25 +0100 Subject: [PATCH 04/19] added signals channel to runner --- runner.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/runner.go b/runner.go index 36ecba3..6de3652 100644 --- a/runner.go +++ b/runner.go @@ -13,12 +13,19 @@ import ( type Runner struct { Sections []*Section DoneChan chan bool + SigChan chan os.Signal } func newRunner() *Runner { - return &Runner{ + r := &Runner{ DoneChan: make(chan bool), + SigChan: make(chan os.Signal), } + + signal.Notify(r.SigChan, os.Interrupt) + signal.Notify(r.SigChan, os.Kill) + + return r } func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { @@ -66,14 +73,11 @@ func (r *Runner) Stop() { func (r *Runner) ListenForSignals() { logger.log("Listening for signals") - sc := make(chan os.Signal, 1) - signal.Notify(sc, os.Interrupt) - signal.Notify(sc, os.Kill) - <-sc + <-r.SigChan fmt.Printf("Interrupt a second time to quit\n") logger.log("Waiting for a second signal") select { - case <-sc: + case <-r.SigChan: logger.log("Second signal received") r.DoneChan <- true case <-time.After(1 * time.Second): From b1a9e43995056ab9fcc5eba4e0c7af56e16147e9 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 1 May 2014 13:44:05 +0100 Subject: [PATCH 05/19] colored logger --- logger.go | 56 ++++++++++++++++++++++++++++++++++++++++--------------- main.go | 2 +- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/logger.go b/logger.go index 42f2e5d..8e80b05 100644 --- a/logger.go +++ b/logger.go @@ -3,13 +3,35 @@ package main import ( "fmt" "log" + "math" "os" "time" ) +var loggerColors = map[string]int{ + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, +} + +var loggerAvailableColors = []string{ + "cyan", + "yellow", + "green", + "magenta", + "red", + "blue", +} + type Logger struct { - Name string - Verbose bool + Name string + Verbose bool + Color int *log.Logger } @@ -19,25 +41,30 @@ func (logger *Logger) Write(p []byte) (n int, err error) { } var ( - loggersCount int + loggerColorIndex int loggerMaxNameLength int ) func newLogger(name string) *Logger { - _logger := log.New(os.Stderr, "", 0) + colorIndex := int(math.Mod(float64(loggerColorIndex), float64(len(loggerAvailableColors)))) + colorName := loggerAvailableColors[colorIndex] - logger := &Logger{ - Name: name, - Logger: _logger, - Verbose: true, - } + loggerColorIndex++ - loggersCount++ if length := len(name); length > loggerMaxNameLength { loggerMaxNameLength = length } - return logger + return newLoggerWithColor(name, colorName) +} + +func newLoggerWithColor(name, colorName string) *Logger { + return &Logger{ + Name: name, + Logger: log.New(os.Stderr, "", 0), + Verbose: true, + Color: loggerColors[colorName], + } } func (logger *Logger) log(format string, v ...interface{}) { @@ -46,9 +73,8 @@ func (logger *Logger) log(format string, v ...interface{}) { } now := time.Now() timeString := fmt.Sprintf("%d:%d:%02d", now.Hour(), now.Minute(), now.Second()) - // format = fmt.Sprintf("%s%s %s |%s %s", color, timeString, prefix, clear, format) - // logger.Logger.Printf(format, v...) - prefix := fmt.Sprintf("%s %-10s |", timeString, logger.Name) - format = fmt.Sprintf("%s %s", prefix, format) + formatPadding := fmt.Sprintf("%%-%ds", loggerMaxNameLength) + prefix := fmt.Sprintf(formatPadding, logger.Name) + format = fmt.Sprintf("\033[%dm%s %s | \033[0m%s", logger.Color, timeString, prefix, format) logger.Logger.Printf(format, v...) } diff --git a/main.go b/main.go index 28ea600..62175fe 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( var logger *Logger func init() { - logger = newLogger("fresh") + logger = newLoggerWithColor("fresh", "white") flag.BoolVar(&logger.Verbose, "verbose", false, "verbose") flag.Parse() } From 35087c14383576d2bd21ecc321d944bfc81631da Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 29 May 2014 16:07:55 +0100 Subject: [PATCH 06/19] -f option to specify Freshfile path --- main.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 62175fe..517fba6 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "os" ) @@ -9,14 +10,18 @@ var logger *Logger func init() { logger = newLoggerWithColor("fresh", "white") - flag.BoolVar(&logger.Verbose, "verbose", false, "verbose") - flag.Parse() } func main() { - r, err := newRunnerWithFreshfile("Freshfile") + var freshfilePath string + + flag.BoolVar(&logger.Verbose, "v", false, "verbose") + flag.StringVar(&freshfilePath, "f", "./Freshfile", "Freshfile path") + flag.Parse() + + r, err := newRunnerWithFreshfile(freshfilePath) if err != nil { - logger.log("%s\n", err.Error()) + fmt.Printf("%s\n", err) os.Exit(1) } From 615ad784ea537616c4ddaead2052de750643dd62 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 29 May 2014 16:18:12 +0100 Subject: [PATCH 07/19] added Makefile --- Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6410b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +GO_CMD=go +GOLINT_CMD=golint +GO_TEST=$(GO_CMD) test -v ./... +GO_VET=$(GO_CMD) vet ./... +GO_LINT=$(GOLINT_CMD) . + +all: + $(GO_VET) + $(GO_LINT) + $(GO_TEST) From 6001c30a14e7a351e49f3da51dd83aea516cb1f2 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 29 May 2014 16:53:32 +0100 Subject: [PATCH 08/19] types are not exported anymore --- command.go | 16 ++++++++-------- logger.go | 12 ++++++------ main.go | 2 +- runner.go | 20 ++++++++++---------- section.go | 16 ++++++++-------- section_test.go | 2 +- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/command.go b/command.go index 71e8763..61826bf 100644 --- a/command.go +++ b/command.go @@ -7,26 +7,26 @@ import ( "strings" ) -type Command struct { +type command struct { Name string CmdString string Cmd *exec.Cmd Stdout io.ReadCloser Stderr io.ReadCloser - Logger *Logger + Logger *customLogger } -func newCommand(name, command string) *Command { - c := &Command{ +func newCommand(name, cmd string) *command { + c := &command{ Name: name, - CmdString: command, + CmdString: cmd, Logger: newLogger(name), } return c } -func (c *Command) build() error { +func (c *command) build() error { options := strings.Split(c.CmdString, " ") c.Cmd = exec.Command(options[0], options[1:]...) @@ -44,7 +44,7 @@ func (c *Command) build() error { return nil } -func (c *Command) Run() error { +func (c *command) Run() error { logger.log("Running command %v\n", c.Name) err := c.build() @@ -63,7 +63,7 @@ func (c *Command) Run() error { return err } -func (c *Command) Stop() { +func (c *command) Stop() { if c.Cmd != nil && c.Cmd.Process != nil { logger.log("Killing process `%s`\n", c.Name) c.Cmd.Process.Kill() diff --git a/logger.go b/logger.go index 8e80b05..1e12aed 100644 --- a/logger.go +++ b/logger.go @@ -28,14 +28,14 @@ var loggerAvailableColors = []string{ "blue", } -type Logger struct { +type customLogger struct { Name string Verbose bool Color int *log.Logger } -func (logger *Logger) Write(p []byte) (n int, err error) { +func (logger *customLogger) Write(p []byte) (n int, err error) { logger.log(string(p)) return len(p), nil } @@ -45,7 +45,7 @@ var ( loggerMaxNameLength int ) -func newLogger(name string) *Logger { +func newLogger(name string) *customLogger { colorIndex := int(math.Mod(float64(loggerColorIndex), float64(len(loggerAvailableColors)))) colorName := loggerAvailableColors[colorIndex] @@ -58,8 +58,8 @@ func newLogger(name string) *Logger { return newLoggerWithColor(name, colorName) } -func newLoggerWithColor(name, colorName string) *Logger { - return &Logger{ +func newLoggerWithColor(name, colorName string) *customLogger { + return &customLogger{ Name: name, Logger: log.New(os.Stderr, "", 0), Verbose: true, @@ -67,7 +67,7 @@ func newLoggerWithColor(name, colorName string) *Logger { } } -func (logger *Logger) log(format string, v ...interface{}) { +func (logger *customLogger) log(format string, v ...interface{}) { if !logger.Verbose { return } diff --git a/main.go b/main.go index 517fba6..00c4f78 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ import ( "os" ) -var logger *Logger +var logger *customLogger func init() { logger = newLoggerWithColor("fresh", "white") diff --git a/runner.go b/runner.go index 6de3652..b81bcdb 100644 --- a/runner.go +++ b/runner.go @@ -10,14 +10,14 @@ import ( "github.com/pilu/config" ) -type Runner struct { - Sections []*Section +type runner struct { + Sections []*section DoneChan chan bool SigChan chan os.Signal } -func newRunner() *Runner { - r := &Runner{ +func newRunner() *runner { + r := &runner{ DoneChan: make(chan bool), SigChan: make(chan os.Signal), } @@ -28,7 +28,7 @@ func newRunner() *Runner { return r } -func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { +func newRunnerWithFreshfile(freshfilePath string) (*runner, error) { r := newRunner() sections, err := config.ParseFile(freshfilePath, "main: *") @@ -46,32 +46,32 @@ func newRunnerWithFreshfile(freshfilePath string) (*Runner, error) { return r, nil } -func (r *Runner) NewSection(description string) *Section { +func (r *runner) NewSection(description string) *section { s := newSection(description) r.Sections = append(r.Sections, s) return s } -func (r *Runner) Run() { +func (r *runner) Run() { logger.log("Running...") logger.log("%d goroutines", runtime.NumGoroutine()) go r.ListenForSignals() for _, s := range r.Sections { - go func(s *Section) { + go func(s *section) { s.Run() }(s) } } -func (r *Runner) Stop() { +func (r *runner) Stop() { logger.log("Stopping all sections") for _, s := range r.Sections { s.Stop() } } -func (r *Runner) ListenForSignals() { +func (r *runner) ListenForSignals() { logger.log("Listening for signals") <-r.SigChan fmt.Printf("Interrupt a second time to quit\n") diff --git a/section.go b/section.go index 82cb6d5..6ee39e5 100644 --- a/section.go +++ b/section.go @@ -5,13 +5,13 @@ import ( "strings" ) -type Section struct { +type section struct { Name string Extensions []string - Commands []*Command + Commands []*command } -func newSection(description string) *Section { +func newSection(description string) *section { var name string parts := strings.Split(description, ":") if len(parts) > 1 { @@ -28,19 +28,19 @@ func newSection(description string) *Section { } } - return &Section{ + return §ion{ Name: name, Extensions: extensions, } } -func (s *Section) NewCommand(name, command string) *Command { - c := newCommand(name, command) +func (s *section) NewCommand(name, cmd string) *command { + c := newCommand(name, cmd) s.Commands = append(s.Commands, c) return c } -func (s *Section) Run() { +func (s *section) Run() { logger.log("Running section `%s`", s.Name) for _, c := range s.Commands { err := c.Run() @@ -51,7 +51,7 @@ func (s *Section) Run() { logger.log("Section ended `%s`", s.Name) } -func (s *Section) Stop() { +func (s *section) Stop() { logger.log("Stopping section %s", s.Name) for _, c := range s.Commands { c.Stop() diff --git a/section_test.go b/section_test.go index 75cd8e4..4c78ae1 100644 --- a/section_test.go +++ b/section_test.go @@ -7,7 +7,7 @@ import ( ) func TestNewSection(t *testing.T) { - var s *Section + var s *section s = newSection(".go,.tpl, .tmpl, .html, , , ") assert.Equal(t, "", s.Name) From d948cefd5ab2cb0170522092b020721c932b7eaa Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 29 Oct 2014 17:39:09 +0000 Subject: [PATCH 09/19] added config --- config.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ config_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 config.go create mode 100644 config_test.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..3738e47 --- /dev/null +++ b/config.go @@ -0,0 +1,69 @@ +package main + +import ( + "bufio" + "io" + "regexp" + "strings" +) + +var commentSplitRegexp = regexp.MustCompile(`[#;]`) + +var keyValueSplitRegexp = regexp.MustCompile(`(\s*(:|=)\s*)|\s+`) + +func cleanConfigLine(line string) string { + chunks := commentSplitRegexp.Split(line, 2) + return strings.TrimSpace(chunks[0]) +} + +func parseConfig(reader *bufio.Reader, mainSectionName string) ([]*section, error) { + var sections []*section + s := newSection("MAIN") + + for { + line, err := reader.ReadString('\n') + if err != nil && err == io.EOF { + break + } else if err != nil { + return sections, err + } + + line = cleanConfigLine(line) + + if len(line) == 0 { + continue + } + + if line[0] == '[' && line[len(line)-1] == ']' { + sections = append(sections, s) + sectionName := line[1:(len(line) - 1)] + s = newSection(sectionName) + } else { + values := keyValueSplitRegexp.Split(line, 2) + key := values[0] + value := "" + if len(values) == 2 { + value = values[1] + } + + s.NewCommand(key, value) + } + } + + sections = append(sections, s) + + return sections, nil +} + +// func parseConfigFile(path string, mainSectionName string) (configSections, error) { +// file, err := os.Open(path) +// if err != nil { +// return make(configSections), err +// } + +// defer file.Close() + +// reader := bufio.NewReader(file) + +// return parseConfig(reader, mainSectionName) +// } diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..fc93519 --- /dev/null +++ b/config_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "bufio" + "strings" + "testing" + + assert "github.com/pilu/miniassert" +) + +func TestParseConfig(t *testing.T) { + content := ` + # comment 1 + ; comment 2 + + foo 1 + bar 2 + + [section_1] + + foo 3 # using spaces after the key + bar 4 # using tabs after the key + # other options for section_1 after section_2 + + [section_2] + a:1 + b: 2 + c : 3 + d :4 + e=5 + f= 6 + g = 7 + h =8 + + url: http://example.com + + [section_3] + ` + reader := bufio.NewReader(strings.NewReader(content)) + sections, err := parseConfig(reader, "main") + + assert.Nil(t, err) + assert.Equal(t, 4, len(sections)) + + // Main section + // mainSection := sections[0] + // assert.Equal(t, 2, len(mainSection.Commands)) + // assert.Equal(t, "1", mainSection["foo"]) + // assert.Equal(t, "2", mainSection["bar"]) + + // // Section 1 + // section1 := sections["section_1"] + // assert.Equal(t, 5, len(section1)) + // assert.Equal(t, "3", section1["foo"]) + // assert.Equal(t, "4", section1["bar"]) + // assert.Equal(t, "5 6", section1["baz"]) + // assert.Equal(t, "7", section1["qux"]) + // assert.Equal(t, "", section1["quux"]) + + // // Section 2 + // section2 := sections["section_2"] + // assert.Equal(t, 9, len(section2)) + // assert.Equal(t, "1", section2["a"]) + // assert.Equal(t, "2", section2["b"]) + // assert.Equal(t, "3", section2["c"]) + // assert.Equal(t, "4", section2["d"]) + // assert.Equal(t, "5", section2["e"]) + // assert.Equal(t, "6", section2["f"]) + // assert.Equal(t, "7", section2["g"]) + // assert.Equal(t, "8", section2["h"]) + // assert.Equal(t, "http://example.com", section2["url"]) + + // // Section 3 + // section3 := sections["section_3"] + // assert.Equal(t, 0, len(section3)) +} From 42a704388902e9672465a8cd5fadca09ad10d2df Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 29 Oct 2014 17:39:48 +0000 Subject: [PATCH 10/19] section can be specified with only name or with name and extensions. --- section.go | 6 +++--- section_test.go | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/section.go b/section.go index 6ee39e5..1e056c2 100644 --- a/section.go +++ b/section.go @@ -11,9 +11,9 @@ type section struct { Commands []*command } -func newSection(description string) *section { - var name string - parts := strings.Split(description, ":") +func newSection(name string) *section { + var description string + parts := strings.SplitN(name, ":", 2) if len(parts) > 1 { name = parts[0] description = parts[1] diff --git a/section_test.go b/section_test.go index 4c78ae1..85c4430 100644 --- a/section_test.go +++ b/section_test.go @@ -9,15 +9,16 @@ import ( func TestNewSection(t *testing.T) { var s *section - s = newSection(".go,.tpl, .tmpl, .html, , , ") - assert.Equal(t, "", s.Name) - assert.Equal(t, []string{".go", ".tpl", ".tmpl", ".html"}, s.Extensions) - assert.Equal(t, 0, len(s.Commands)) - - s = newSection("stylesheets: .css, .less") + s = newSection("stylesheets: .css, .less, , , ") assert.Equal(t, "stylesheets", s.Name) assert.Equal(t, []string{".css", ".less"}, s.Extensions) assert.Equal(t, 0, len(s.Commands)) + + // only name, without extensions + s = newSection("foo-section") + assert.Equal(t, "foo-section", s.Name) + assert.Equal(t, 0, len(s.Extensions)) + assert.Equal(t, 0, len(s.Commands)) } func TestSection_NewCommand(t *testing.T) { From 04dbc5f122b33c50b85d4973f02280e6cf867974 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 30 Oct 2014 00:47:51 +0100 Subject: [PATCH 11/19] using new config code --- command.go | 2 +- config.go | 29 +++++++++++++++-------------- logger.go | 2 +- runner.go | 17 ++--------------- runner_test.go | 10 ++-------- 5 files changed, 21 insertions(+), 39 deletions(-) diff --git a/command.go b/command.go index 61826bf..bbc56f3 100644 --- a/command.go +++ b/command.go @@ -57,7 +57,7 @@ func (c *command) Run() error { err = c.Cmd.Run() if err != nil { - logger.log("Errors on `%s`: %v\n", c.Name, err) + log.Fatal(err) } return err diff --git a/config.go b/config.go index 3738e47..924f689 100644 --- a/config.go +++ b/config.go @@ -3,22 +3,23 @@ package main import ( "bufio" "io" + "os" "regexp" "strings" ) -var commentSplitRegexp = regexp.MustCompile(`[#;]`) +var configCommentSplitRegexp = regexp.MustCompile(`[#;]`) -var keyValueSplitRegexp = regexp.MustCompile(`(\s*(:|=)\s*)|\s+`) +var configKeyValueSplitRegexp = regexp.MustCompile(`(\s*(:|=)\s*)|\s+`) func cleanConfigLine(line string) string { - chunks := commentSplitRegexp.Split(line, 2) + chunks := configCommentSplitRegexp.Split(line, 2) return strings.TrimSpace(chunks[0]) } func parseConfig(reader *bufio.Reader, mainSectionName string) ([]*section, error) { var sections []*section - s := newSection("MAIN") + s := newSection(mainSectionName) for { line, err := reader.ReadString('\n') @@ -39,7 +40,7 @@ func parseConfig(reader *bufio.Reader, mainSectionName string) ([]*section, erro sectionName := line[1:(len(line) - 1)] s = newSection(sectionName) } else { - values := keyValueSplitRegexp.Split(line, 2) + values := configKeyValueSplitRegexp.Split(line, 2) key := values[0] value := "" if len(values) == 2 { @@ -55,15 +56,15 @@ func parseConfig(reader *bufio.Reader, mainSectionName string) ([]*section, erro return sections, nil } -// func parseConfigFile(path string, mainSectionName string) (configSections, error) { -// file, err := os.Open(path) -// if err != nil { -// return make(configSections), err -// } +func parseConfigFile(path string, mainSectionName string) ([]*section, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } -// defer file.Close() + defer file.Close() -// reader := bufio.NewReader(file) + reader := bufio.NewReader(file) -// return parseConfig(reader, mainSectionName) -// } + return parseConfig(reader, mainSectionName) +} diff --git a/logger.go b/logger.go index 1e12aed..e8667dc 100644 --- a/logger.go +++ b/logger.go @@ -72,7 +72,7 @@ func (logger *customLogger) log(format string, v ...interface{}) { return } now := time.Now() - timeString := fmt.Sprintf("%d:%d:%02d", now.Hour(), now.Minute(), now.Second()) + timeString := fmt.Sprintf("%02d:%02d:%02d", now.Hour(), now.Minute(), now.Second()) formatPadding := fmt.Sprintf("%%-%ds", loggerMaxNameLength) prefix := fmt.Sprintf(formatPadding, logger.Name) format = fmt.Sprintf("\033[%dm%s %s | \033[0m%s", logger.Color, timeString, prefix, format) diff --git a/runner.go b/runner.go index b81bcdb..df67eba 100644 --- a/runner.go +++ b/runner.go @@ -6,8 +6,6 @@ import ( "os/signal" "runtime" "time" - - "github.com/pilu/config" ) type runner struct { @@ -31,27 +29,16 @@ func newRunner() *runner { func newRunnerWithFreshfile(freshfilePath string) (*runner, error) { r := newRunner() - sections, err := config.ParseFile(freshfilePath, "main: *") + sections, err := parseConfigFile(freshfilePath, "main: *") if err != nil { return r, err } - for s, opts := range sections { - section := r.NewSection(s) - for name, cmd := range opts { - section.NewCommand(name, cmd) - } - } + r.Sections = sections return r, nil } -func (r *runner) NewSection(description string) *section { - s := newSection(description) - r.Sections = append(r.Sections, s) - return s -} - func (r *runner) Run() { logger.log("Running...") logger.log("%d goroutines", runtime.NumGoroutine()) diff --git a/runner_test.go b/runner_test.go index 02d2e64..70f12dc 100644 --- a/runner_test.go +++ b/runner_test.go @@ -1,8 +1,9 @@ package main import ( - assert "github.com/pilu/miniassert" "testing" + + assert "github.com/pilu/miniassert" ) func TestNewRunner(t *testing.T) { @@ -10,13 +11,6 @@ func TestNewRunner(t *testing.T) { assert.Equal(t, 0, len(r.Sections)) } -func TestRunner_NewSection(t *testing.T) { - r := newRunner() - s := r.NewSection(".go") - assert.Equal(t, 1, len(r.Sections)) - assert.Equal(t, s, r.Sections[0]) -} - func TestNewRunnerWithFreshfile_WithFileNotFound(t *testing.T) { _, err := newRunnerWithFreshfile("./_test_fixtures/file-not-found") assert.NotNil(t, err) From f8db5ba28448e3775d0b4c985f8cecf8be9a379b Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 30 Oct 2014 01:53:36 +0100 Subject: [PATCH 12/19] updated log call --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index bbc56f3..61826bf 100644 --- a/command.go +++ b/command.go @@ -57,7 +57,7 @@ func (c *command) Run() error { err = c.Cmd.Run() if err != nil { - log.Fatal(err) + logger.log("Errors on `%s`: %v\n", c.Name, err) } return err From 435517eeb5749adcfe318876a7a3667d80ea0008 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 30 Oct 2014 11:26:00 +0000 Subject: [PATCH 13/19] it prints section name before command name --- command.go | 8 ++++++-- command_test.go | 9 ++++++--- section.go | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/command.go b/command.go index 61826bf..3dbe32e 100644 --- a/command.go +++ b/command.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "log" "os/exec" @@ -8,6 +9,7 @@ import ( ) type command struct { + Section *section Name string CmdString string Cmd *exec.Cmd @@ -16,11 +18,13 @@ type command struct { Logger *customLogger } -func newCommand(name, cmd string) *command { +func newCommand(section *section, name, cmd string) *command { + loggerPrefix := fmt.Sprintf("%s - %s", section.Name, name) c := &command{ + Section: section, Name: name, CmdString: cmd, - Logger: newLogger(name), + Logger: newLogger(loggerPrefix), } return c diff --git a/command_test.go b/command_test.go index a923f4d..e0a772a 100644 --- a/command_test.go +++ b/command_test.go @@ -1,18 +1,21 @@ package main import ( - assert "github.com/pilu/miniassert" "testing" + + assert "github.com/pilu/miniassert" ) func TestNewCommand(t *testing.T) { - c := newCommand("build", "./build all -o foo") + s := newSection("foo") + c := newCommand(s, "build", "./build all -o foo") assert.Equal(t, "build", c.Name) assert.Equal(t, "./build all -o foo", c.CmdString) } func TestCommand_Build(t *testing.T) { - c := newCommand("build", "./build all -o foo") + s := newSection("foo") + c := newCommand(s, "build", "./build all -o foo") assert.Nil(t, c.Cmd) c.build() diff --git a/section.go b/section.go index 1e056c2..d5fb5c2 100644 --- a/section.go +++ b/section.go @@ -35,7 +35,7 @@ func newSection(name string) *section { } func (s *section) NewCommand(name, cmd string) *command { - c := newCommand(name, cmd) + c := newCommand(s, name, cmd) s.Commands = append(s.Commands, c) return c } From 4ad5c580609cd312156e27554d3ffedef2d82d52 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 30 Oct 2014 15:25:14 +0000 Subject: [PATCH 14/19] better logs --- command.go | 15 ++++++++++++--- main.go | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/command.go b/command.go index 3dbe32e..be85e64 100644 --- a/command.go +++ b/command.go @@ -49,7 +49,7 @@ func (c *command) build() error { } func (c *command) Run() error { - logger.log("Running command %v\n", c.Name) + logger.log("Running command %s: %s\n", c.Name, c.CmdString) err := c.build() if err != nil { @@ -59,11 +59,20 @@ func (c *command) Run() error { go io.Copy(c.Logger, c.Stdout) go io.Copy(c.Logger, c.Stderr) - err = c.Cmd.Run() + err = c.Cmd.Start() if err != nil { - logger.log("Errors on `%s`: %v\n", c.Name, err) + logger.log("Errors on `%s - %s`: %v\n", c.Section.Name, c.Name, err) } + logger.log(fmt.Sprintf("`%s - %s` started with pid %d", c.Section.Name, c.Name, c.Cmd.Process.Pid)) + + err = c.Cmd.Wait() + if err != nil { + logger.log("Errors on `%s - %s`: %v\n", c.Section.Name, c.Name, err) + } + + logger.log("`%s - %s` ended\n", c.Section.Name, c.Name) + return err } diff --git a/main.go b/main.go index 00c4f78..04393c6 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ func main() { os.Exit(1) } - logger.log("Process %d", os.Getpid()) + logger.log("started with pid %d", os.Getpid()) r.Run() <-r.DoneChan println("the end") From 9d3c332d112a35eb572c048ec20d22c8f5412c1d Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Fri, 7 Nov 2014 15:01:33 +0000 Subject: [PATCH 15/19] default config filename const --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 04393c6..f21b477 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "os" ) +const defaultConfigFilename = "Freshfile" + var logger *customLogger func init() { @@ -16,7 +18,7 @@ func main() { var freshfilePath string flag.BoolVar(&logger.Verbose, "v", false, "verbose") - flag.StringVar(&freshfilePath, "f", "./Freshfile", "Freshfile path") + flag.StringVar(&freshfilePath, "f", defaultConfigFilename, "Freshfile path") flag.Parse() r, err := newRunnerWithFreshfile(freshfilePath) From 9462eea47d2aabe90e456f0eccfca0452bebc131 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 13 Nov 2014 19:01:29 +0000 Subject: [PATCH 16/19] more test on configuration --- config_test.go | 81 +++++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/config_test.go b/config_test.go index fc93519..3cfb376 100644 --- a/config_test.go +++ b/config_test.go @@ -36,6 +36,7 @@ func TestParseConfig(t *testing.T) { [section_3] ` + reader := bufio.NewReader(strings.NewReader(content)) sections, err := parseConfig(reader, "main") @@ -43,34 +44,54 @@ func TestParseConfig(t *testing.T) { assert.Equal(t, 4, len(sections)) // Main section - // mainSection := sections[0] - // assert.Equal(t, 2, len(mainSection.Commands)) - // assert.Equal(t, "1", mainSection["foo"]) - // assert.Equal(t, "2", mainSection["bar"]) - - // // Section 1 - // section1 := sections["section_1"] - // assert.Equal(t, 5, len(section1)) - // assert.Equal(t, "3", section1["foo"]) - // assert.Equal(t, "4", section1["bar"]) - // assert.Equal(t, "5 6", section1["baz"]) - // assert.Equal(t, "7", section1["qux"]) - // assert.Equal(t, "", section1["quux"]) - - // // Section 2 - // section2 := sections["section_2"] - // assert.Equal(t, 9, len(section2)) - // assert.Equal(t, "1", section2["a"]) - // assert.Equal(t, "2", section2["b"]) - // assert.Equal(t, "3", section2["c"]) - // assert.Equal(t, "4", section2["d"]) - // assert.Equal(t, "5", section2["e"]) - // assert.Equal(t, "6", section2["f"]) - // assert.Equal(t, "7", section2["g"]) - // assert.Equal(t, "8", section2["h"]) - // assert.Equal(t, "http://example.com", section2["url"]) - - // // Section 3 - // section3 := sections["section_3"] - // assert.Equal(t, 0, len(section3)) + mainSection := sections[0] + assert.Equal(t, 2, len(mainSection.Commands)) + tests := [][]string{ + {"foo", "1"}, + {"bar", "2"}, + } + + for i, opts := range tests { + assert.Equal(t, opts[0], mainSection.Commands[i].Name) + assert.Equal(t, opts[1], mainSection.Commands[i].CmdString) + } + + // Section 1 + section1 := sections[1] + assert.Equal(t, "section_1", section1.Name) + tests = [][]string{ + {"foo", "3"}, + {"bar", "4"}, + } + + for i, opts := range tests { + assert.Equal(t, opts[0], section1.Commands[i].Name) + assert.Equal(t, opts[1], section1.Commands[i].CmdString) + } + + // Section 2 + section2 := sections[2] + assert.Equal(t, "section_2", section2.Name) + assert.Equal(t, 9, len(section2.Commands)) + tests = [][]string{ + {"a", "1"}, + {"b", "2"}, + {"c", "3"}, + {"d", "4"}, + {"e", "5"}, + {"f", "6"}, + {"g", "7"}, + {"h", "8"}, + {"url", "http://example.com"}, + } + + for i, opts := range tests { + assert.Equal(t, opts[0], section2.Commands[i].Name) + assert.Equal(t, opts[1], section2.Commands[i].CmdString) + } + + // Section 3 + section3 := sections[3] + assert.Equal(t, "section_3", section3.Name) + assert.Equal(t, 0, len(section3.Commands)) } From 8b6472bf9c388e1fe51284ac2c5e08c6746aacb4 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 13 Nov 2014 19:08:54 +0000 Subject: [PATCH 17/19] section has globs instead of extensions --- section.go | 29 +++++++++++++++-------------- section_test.go | 8 ++++---- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/section.go b/section.go index d5fb5c2..c8769fe 100644 --- a/section.go +++ b/section.go @@ -6,31 +6,32 @@ import ( ) type section struct { - Name string - Extensions []string - Commands []*command + Name string + Globs []string + Commands []*command } -func newSection(name string) *section { - var description string - parts := strings.SplitN(name, ":", 2) +func newSection(description string) *section { + var globsString string + name := description + parts := strings.SplitN(description, ":", 2) if len(parts) > 1 { name = parts[0] - description = parts[1] + globsString = parts[1] } extRe := regexp.MustCompile(`\s*,\s*`) - extensions := []string{} - for _, rawExtension := range extRe.Split(description, -1) { - extension := strings.TrimSpace(rawExtension) - if len(extension) > 0 { - extensions = append(extensions, extension) + globs := []string{} + for _, rawGlob := range extRe.Split(globsString, -1) { + glob := strings.TrimSpace(rawGlob) + if len(glob) > 0 { + globs = append(globs, glob) } } return §ion{ - Name: name, - Extensions: extensions, + Name: name, + Globs: globs, } } diff --git a/section_test.go b/section_test.go index 85c4430..3fe8b57 100644 --- a/section_test.go +++ b/section_test.go @@ -9,15 +9,15 @@ import ( func TestNewSection(t *testing.T) { var s *section - s = newSection("stylesheets: .css, .less, , , ") + s = newSection("stylesheets: *.css, *.less, , , ") assert.Equal(t, "stylesheets", s.Name) - assert.Equal(t, []string{".css", ".less"}, s.Extensions) + assert.Equal(t, []string{"*.css", "*.less"}, s.Globs) assert.Equal(t, 0, len(s.Commands)) - // only name, without extensions + // only name, without globs s = newSection("foo-section") assert.Equal(t, "foo-section", s.Name) - assert.Equal(t, 0, len(s.Extensions)) + assert.Equal(t, 0, len(s.Globs)) assert.Equal(t, 0, len(s.Commands)) } From dcd3f7930c95b27d2699a80b63cccca064d1ed45 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 13 Nov 2014 19:28:11 +0000 Subject: [PATCH 18/19] prints CmdString before running it --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index be85e64..e3982ed 100644 --- a/command.go +++ b/command.go @@ -49,7 +49,7 @@ func (c *command) build() error { } func (c *command) Run() error { - logger.log("Running command %s: %s\n", c.Name, c.CmdString) + c.Logger.log(c.CmdString) err := c.build() if err != nil { From 7de5f7b3f47fbdcdec121a960fd218c0b58725bd Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 8 Jul 2015 16:57:12 +0100 Subject: [PATCH 19/19] WIP --- command.go | 9 ++- command_test.go | 36 +++++----- config.go | 174 ++++++++++++++++++++++++++++++++++++------------ config_test.go | 102 ++++++++-------------------- runner.go | 14 ++-- section.go | 5 +- section_test.go | 44 ++++++------ 7 files changed, 212 insertions(+), 172 deletions(-) diff --git a/command.go b/command.go index e3982ed..f7d8d91 100644 --- a/command.go +++ b/command.go @@ -18,7 +18,14 @@ type command struct { Logger *customLogger } -func newCommand(section *section, name, cmd string) *command { +func newCommand(section *section, cmd string) *command { + var name string + parts := strings.Split(cmd, " ") + + if len(parts) > 0 { + name = parts[0] + } + loggerPrefix := fmt.Sprintf("%s - %s", section.Name, name) c := &command{ Section: section, diff --git a/command_test.go b/command_test.go index e0a772a..3ac7be1 100644 --- a/command_test.go +++ b/command_test.go @@ -1,25 +1,19 @@ package main -import ( - "testing" +// func TestNewCommand(t *testing.T) { +// s := newSection("foo") +// c := newCommand(s, "build", "./build all -o foo") +// assert.Equal(t, "build", c.Name) +// assert.Equal(t, "./build all -o foo", c.CmdString) +// } - assert "github.com/pilu/miniassert" -) +// func TestCommand_Build(t *testing.T) { +// s := newSection("foo") +// c := newCommand(s, "build", "./build all -o foo") +// assert.Nil(t, c.Cmd) -func TestNewCommand(t *testing.T) { - s := newSection("foo") - c := newCommand(s, "build", "./build all -o foo") - assert.Equal(t, "build", c.Name) - assert.Equal(t, "./build all -o foo", c.CmdString) -} - -func TestCommand_Build(t *testing.T) { - s := newSection("foo") - c := newCommand(s, "build", "./build all -o foo") - assert.Nil(t, c.Cmd) - - c.build() - assert.NotNil(t, c.Cmd) - assert.Equal(t, "./build", c.Cmd.Path) - assert.Equal(t, []string{"./build", "all", "-o", "foo"}, c.Cmd.Args) -} +// c.build() +// assert.NotNil(t, c.Cmd) +// assert.Equal(t, "./build", c.Cmd.Path) +// assert.Equal(t, []string{"./build", "all", "-o", "foo"}, c.Cmd.Args) +// } diff --git a/config.go b/config.go index 924f689..0b8bc02 100644 --- a/config.go +++ b/config.go @@ -2,69 +2,161 @@ package main import ( "bufio" - "io" - "os" - "regexp" - "strings" + "fmt" + "text/scanner" ) -var configCommentSplitRegexp = regexp.MustCompile(`[#;]`) +type config struct { + sections []*section +} + +type stateFunc func(*config, *section) (stateFunc, *section, error) + +type configScanner struct { + s scanner.Scanner + state stateFunc + commands map[string]stateFunc +} + +func newConfigScanner(r *bufio.Reader) *configScanner { + var sc scanner.Scanner + sc.Init(r) + + s := &configScanner{s: sc} + s.init() + + return s +} + +func (s *configScanner) init() { + s.commands = map[string]stateFunc{ + "RUN": s.scanCMDRun, + "WATCH": s.scanCMDWatch, + } +} -var configKeyValueSplitRegexp = regexp.MustCompile(`(\s*(:|=)\s*)|\s+`) +func (s *configScanner) next() rune { + r := s.s.Next() + if r != '#' { + return r + } + + for r != '\n' && r != scanner.EOF { + r = s.s.Next() + } -func cleanConfigLine(line string) string { - chunks := configCommentSplitRegexp.Split(line, 2) - return strings.TrimSpace(chunks[0]) + return r } -func parseConfig(reader *bufio.Reader, mainSectionName string) ([]*section, error) { - var sections []*section - s := newSection(mainSectionName) +func (s *configScanner) scan(c *config) error { + var err error - for { - line, err := reader.ReadString('\n') - if err != nil && err == io.EOF { + sec := §ion{Name: "MAIN"} + c.sections = append(c.sections, sec) + + for s.state = s.scanLine; s.state != nil; { + s.state, sec, err = s.state(c, sec) + if err != nil { break - } else if err != nil { - return sections, err } + } - line = cleanConfigLine(line) + return err +} - if len(line) == 0 { - continue +func (s *configScanner) scanLine(c *config, sec *section) (stateFunc, *section, error) { + r := s.s.Peek() + for r != scanner.EOF { + if r != ' ' && r != '\t' && r != '\n' && r != '#' { + if r == '[' { + return s.scanSection, sec, nil + } + + return s.scanCMD, sec, nil } - if line[0] == '[' && line[len(line)-1] == ']' { - sections = append(sections, s) - sectionName := line[1:(len(line) - 1)] - s = newSection(sectionName) - } else { - values := configKeyValueSplitRegexp.Split(line, 2) - key := values[0] - value := "" - if len(values) == 2 { - value = values[1] - } + s.next() + r = s.s.Peek() + + } - s.NewCommand(key, value) + return nil, sec, nil +} + +func (s *configScanner) scanSection(c *config, sec *section) (stateFunc, *section, error) { + r := s.next() + if r != '[' { + return nil, sec, s.errorExpectedRune("[") + } + + sec = §ion{} + c.sections = append(c.sections, sec) + + return s.scanSectionName, sec, nil +} + +func (s *configScanner) scanSectionName(c *config, sec *section) (stateFunc, *section, error) { + var name string + r := s.s.Peek() + for r != ']' { + if r == scanner.EOF || r == '\n' || r == '#' { + return nil, sec, s.errorExpectedRune("]") } + + r = s.s.Next() + name += string(r) + r = s.s.Peek() } + s.next() + sec.Name = name - sections = append(sections, s) + return s.scanLine, sec, nil +} - return sections, nil +func (s *configScanner) scanCMD(c *config, sec *section) (stateFunc, *section, error) { + var name string + r := s.next() + for r != scanner.EOF { + if r == ' ' || r == '\t' || r == '\n' { + break + } + + name += string(r) + r = s.next() + } + + if cmd, ok := s.commands[name]; ok { + return cmd, sec, nil + } + + return nil, sec, fmt.Errorf("Unknown command `%s`", name) } -func parseConfigFile(path string, mainSectionName string) ([]*section, error) { - file, err := os.Open(path) - if err != nil { - return nil, err +func (s *configScanner) scanCMDRun(c *config, sec *section) (stateFunc, *section, error) { + var cmdString string + r := s.next() + for r != scanner.EOF && r != '\n' { + cmdString = cmdString + string(r) + r = s.next() } - defer file.Close() + sec.NewCommand(cmdString) - reader := bufio.NewReader(file) + return s.scanLine, sec, nil +} + +func (s *configScanner) scanCMDWatch(c *config, sec *section) (stateFunc, *section, error) { + var cmd string + r := s.next() + for r != scanner.EOF && r != '\n' { + cmd = cmd + string(r) + r = s.next() + } + + return s.scanLine, sec, nil +} - return parseConfig(reader, mainSectionName) +func (s *configScanner) errorExpectedRune(c string) error { + p := s.s.Pos() + return fmt.Errorf("Expected `%s` at line %d, col %d", c, p.Line, p.Column) } diff --git a/config_test.go b/config_test.go index 3cfb376..eb4bd61 100644 --- a/config_test.go +++ b/config_test.go @@ -10,88 +10,38 @@ import ( func TestParseConfig(t *testing.T) { content := ` - # comment 1 - ; comment 2 - - foo 1 - bar 2 - - [section_1] - - foo 3 # using spaces after the key - bar 4 # using tabs after the key - # other options for section_1 after section_2 - - [section_2] - a:1 - b: 2 - c : 3 - d :4 - e=5 - f= 6 - g = 7 - h =8 - - url: http://example.com - - [section_3] - ` + # comment + [section 1] # aslid las dlkj s + WATCH ./public/js + RUN + RUN + [section 2] + [section 3] + #WATCH .` reader := bufio.NewReader(strings.NewReader(content)) - sections, err := parseConfig(reader, "main") - - assert.Nil(t, err) - assert.Equal(t, 4, len(sections)) - // Main section - mainSection := sections[0] - assert.Equal(t, 2, len(mainSection.Commands)) - tests := [][]string{ - {"foo", "1"}, - {"bar", "2"}, - } + cs := newConfigScanner(reader) - for i, opts := range tests { - assert.Equal(t, opts[0], mainSection.Commands[i].Name) - assert.Equal(t, opts[1], mainSection.Commands[i].CmdString) - } - - // Section 1 - section1 := sections[1] - assert.Equal(t, "section_1", section1.Name) - tests = [][]string{ - {"foo", "3"}, - {"bar", "4"}, - } + config := &config{} + err := cs.scan(config) + assert.Nil(t, err) + assert.Equal(t, 4, len(config.sections)) - for i, opts := range tests { - assert.Equal(t, opts[0], section1.Commands[i].Name) - assert.Equal(t, opts[1], section1.Commands[i].CmdString) - } + s := config.sections[0] + assert.Equal(t, "MAIN", s.Name) - // Section 2 - section2 := sections[2] - assert.Equal(t, "section_2", section2.Name) - assert.Equal(t, 9, len(section2.Commands)) - tests = [][]string{ - {"a", "1"}, - {"b", "2"}, - {"c", "3"}, - {"d", "4"}, - {"e", "5"}, - {"f", "6"}, - {"g", "7"}, - {"h", "8"}, - {"url", "http://example.com"}, - } + s = config.sections[1] + assert.Equal(t, "section 1", s.Name) + assert.Equal(t, 2, len(s.Commands)) + assert.Equal(t, "compile-js", s.Commands[0].Name) + // assert.Equal(t, "compile-js -w ./public/javascripts", s.Commands[0].CmdString) + // assert.Equal(t, "minify-js", s.Commands[1].Name) + // assert.Equal(t, "minify-js ./public/javascripts/app.js", s.Commands[1].CmdString) - for i, opts := range tests { - assert.Equal(t, opts[0], section2.Commands[i].Name) - assert.Equal(t, opts[1], section2.Commands[i].CmdString) - } + s = config.sections[2] + assert.Equal(t, "section 2", s.Name) - // Section 3 - section3 := sections[3] - assert.Equal(t, "section_3", section3.Name) - assert.Equal(t, 0, len(section3.Commands)) + s = config.sections[3] + assert.Equal(t, "section 3", s.Name) } diff --git a/runner.go b/runner.go index df67eba..806185a 100644 --- a/runner.go +++ b/runner.go @@ -29,14 +29,16 @@ func newRunner() *runner { func newRunnerWithFreshfile(freshfilePath string) (*runner, error) { r := newRunner() - sections, err := parseConfigFile(freshfilePath, "main: *") - if err != nil { - return r, err - } + return r, nil - r.Sections = sections + // sections, err := parseConfigFile(freshfilePath, "main: *") + // if err != nil { + // return r, err + // } - return r, nil + // r.Sections = sections + + // return r, nil } func (r *runner) Run() { diff --git a/section.go b/section.go index c8769fe..5a69e80 100644 --- a/section.go +++ b/section.go @@ -35,9 +35,10 @@ func newSection(description string) *section { } } -func (s *section) NewCommand(name, cmd string) *command { - c := newCommand(s, name, cmd) +func (s *section) NewCommand(cmd string) *command { + c := newCommand(s, cmd) s.Commands = append(s.Commands, c) + return c } diff --git a/section_test.go b/section_test.go index 3fe8b57..b13cff9 100644 --- a/section_test.go +++ b/section_test.go @@ -1,30 +1,24 @@ package main -import ( - "testing" +// func TestNewSection(t *testing.T) { +// var s *section - assert "github.com/pilu/miniassert" -) +// s = newSection("stylesheets: *.css, *.less, , , ") +// assert.Equal(t, "stylesheets", s.Name) +// assert.Equal(t, []string{"*.css", "*.less"}, s.Globs) +// assert.Equal(t, 0, len(s.Commands)) -func TestNewSection(t *testing.T) { - var s *section +// // only name, without globs +// s = newSection("foo-section") +// assert.Equal(t, "foo-section", s.Name) +// assert.Equal(t, 0, len(s.Globs)) +// assert.Equal(t, 0, len(s.Commands)) +// } - s = newSection("stylesheets: *.css, *.less, , , ") - assert.Equal(t, "stylesheets", s.Name) - assert.Equal(t, []string{"*.css", "*.less"}, s.Globs) - assert.Equal(t, 0, len(s.Commands)) - - // only name, without globs - s = newSection("foo-section") - assert.Equal(t, "foo-section", s.Name) - assert.Equal(t, 0, len(s.Globs)) - assert.Equal(t, 0, len(s.Commands)) -} - -func TestSection_NewCommand(t *testing.T) { - s := newSection("go") - assert.Equal(t, 0, len(s.Commands)) - c := s.NewCommand("build", "./build") - assert.Equal(t, 1, len(s.Commands)) - assert.Equal(t, c, s.Commands[0]) -} +// func TestSection_NewCommand(t *testing.T) { +// s := newSection("go") +// assert.Equal(t, 0, len(s.Commands)) +// c := s.NewCommand("build", "./build") +// assert.Equal(t, 1, len(s.Commands)) +// assert.Equal(t, c, s.Commands[0]) +// }