Skip to content

Commit

Permalink
add local executor and way to test logging
Browse files Browse the repository at this point in the history
  • Loading branch information
pitluga committed Jan 7, 2016
1 parent 0f3cc2d commit 3916844
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 5 deletions.
80 changes: 77 additions & 3 deletions crony/executor.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,92 @@
package crony

import (
"os/exec"
"syscall"

"github.com/mattn/go-shellwords"
)

type Process struct {
command *exec.Cmd
done chan int
}

func (process *Process) Done() chan int {
return process.done
}

type Executor interface {
Execute(command string) error
Execute(command string) (*Process, error)
}

type FakeExecutor struct {
Commands []string
}

func (executor *FakeExecutor) Execute(command string) error {
func (executor *FakeExecutor) Execute(command string) (*Process, error) {
executor.Commands = append(executor.Commands, command)
return nil
return &Process{}, nil
}

func CreateFakeExecutor() *FakeExecutor {
return &FakeExecutor{make([]string, 0)}
}

type LocalExecutor struct {
stdout ReaderConsumer
stderr ReaderConsumer
}

func waitForExitCode(cmd *exec.Cmd, doneChan chan int) {
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
doneChan <- status.ExitStatus()
} else {
panic("Unable to convert to syscall.WaitStatus; this shouldn't happen")
}
} else {
// we failed while attempting to launch process
doneChan <- -1
}
} else {
doneChan <- 0
}
}

func (executor *LocalExecutor) Execute(command string) (*Process, error) {
commandParts, err := shellwords.Parse(command)
if err != nil {
return nil, err
}
cmd := exec.Command(commandParts[0], commandParts[1:]...)

stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}

doneChan := make(chan int)

if err := cmd.Start(); err != nil {
return nil, err
}

go executor.stderr.ConsumeReader(stderr)
go executor.stdout.ConsumeReader(stdout)
go waitForExitCode(cmd, doneChan)

return &Process{
command: cmd,
done: doneChan,
}, nil
}

func NewLocalExecutor(stdout ReaderConsumer, stderr ReaderConsumer) *LocalExecutor {
return &LocalExecutor{stdout, stderr}
}
47 changes: 47 additions & 0 deletions crony/executor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package crony

import (
"testing"
)

func TestFakeExecutorRecordsCommands(t *testing.T) {
executor := CreateFakeExecutor()

_, err := executor.Execute("echo the command")

if err != nil {
t.Error("should not return an error")
}

if len(executor.Commands) != 1 {
t.Error("should have recorded one command")
}

if executor.Commands[0] != "echo the command" {
t.Error("should have recorded the command string")
}
}

func TestLocalExecutorRunsTheCommandOnTheShell(t *testing.T) {
stdoutSink := NewSink()
stderrSink := NewSink()
executor := NewLocalExecutor(stdoutSink, stderrSink)

process, err := executor.Execute("/bin/echo hi")

if err != nil {
t.Error("should not return an error")
}

if exit := <-process.Done(); exit != 0 {
t.Errorf("Should have exited with 0, got %q", exit)
}

if stderrSink.Content != "" {
t.Error("should have gotten no stderr")
}

if stdoutSink.Content != "hi\n" {
t.Errorf("should have gotten hi, got %q", stdoutSink.Content)
}
}
28 changes: 28 additions & 0 deletions crony/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package crony

import (
"io"
"io/ioutil"
)

type ReaderConsumer interface {
ConsumeReader(reader io.Reader) error
}

type Sink struct {
Content string
}

func NewSink() *Sink {
return &Sink{}
}

func (sink *Sink) ConsumeReader(reader io.Reader) error {
bytes, err := ioutil.ReadAll(reader)
if err != nil {
return err
}

sink.Content = string(bytes)
return nil
}
16 changes: 16 additions & 0 deletions crony/io_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package crony

import (
"strings"
"testing"
)

func TestSink(t *testing.T) {
reader := strings.NewReader("this is a string")
sink := NewSink()
sink.ConsumeReader(reader)

if sink.Content != "this is a string" {
t.Error("sink did not properly consume reader")
}
}
5 changes: 3 additions & 2 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package server

import (
"github.com/pitluga/crony/crony"
"testing"
"time"

"github.com/pitluga/crony/crony"
)

func TestOnceWithNoJobsDoesNothing(t *testing.T) {
Expand Down Expand Up @@ -48,7 +49,7 @@ func TestStartStopWithSingleJob(t *testing.T) {
time.Millisecond*10,
)

timer := time.NewTimer(time.Millisecond * 12)
timer := time.NewTimer(time.Millisecond * 15)
<-timer.C

server.Stop()
Expand Down

0 comments on commit 3916844

Please sign in to comment.