diff --git a/.gitignore b/.gitignore index 3791815..79074fb 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ _testmain.go *.prof # Output +coverage.out dockron dockron-* # deps diff --git a/Makefile b/Makefile index 4811de0..4625ed0 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,13 @@ vendor: run: go run . +.PHONY: test +test: + go test -coverprofile=coverage.out + go tool cover -func=coverage.out + # @go tool cover -func=coverage.out | awk -v target=80.0% \ + '/^total:/ { print "Total coverage: " $$3 " Minimum coverage: " target; if ($$3+0.0 >= target+0.0) print "ok"; else { print "fail"; exit 1; } }' + # Output target dockron: @echo Version: $(VERSION) diff --git a/main.go b/main.go index 5ea0b9d..bc8c12a 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "flag" "fmt" dockerTypes "github.com/docker/docker/api/types" - dockerCient "github.com/docker/docker/client" + dockerClient "github.com/docker/docker/client" "github.com/robfig/cron/v3" "golang.org/x/net/context" "log" diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..edba326 --- /dev/null +++ b/main_test.go @@ -0,0 +1,304 @@ +package main + +import ( + "fmt" + dockerTypes "github.com/docker/docker/api/types" + "github.com/robfig/cron/v3" + "golang.org/x/net/context" + "log" + "testing" +) + +// FakeDockerClient is used to test without interracting with Docker +type FakeDockerClient struct { + FakeContainers []dockerTypes.Container +} + +// ContainerStart pretends to start a container +func (fakeClient *FakeDockerClient) ContainerStart(context context.Context, containerID string, options dockerTypes.ContainerStartOptions) error { + return nil +} + +func (fakeClient *FakeDockerClient) ContainerList(context context.Context, options dockerTypes.ContainerListOptions) ([]dockerTypes.Container, error) { + return fakeClient.FakeContainers, nil +} + +// NewFakeDockerClient creates an empty client +func NewFakeDockerClient() *FakeDockerClient { + return &FakeDockerClient{} +} + +// NewFakeDockerClientWithContainers creates a client with the provided containers +func NewFakeDockerClientWithContainers(containers []dockerTypes.Container) *FakeDockerClient { + return &FakeDockerClient{FakeContainers: containers} +} + +// ErrorUnequal checks that two values are equal and fails the test if not +func ErrorUnequal(t *testing.T, expected interface{}, actual interface{}, message string) { + if expected != actual { + t.Errorf("%s Expected: %+v Actual: %+v", message, expected, actual) + } +} + +// TestQueryScheduledJobs checks that when querying the Docker client that we +// create jobs for any containers with a dockron.schedule +func TestQueryScheduledJobs(t *testing.T) { + client := NewFakeDockerClient() + + cases := []struct { + name string + fakeContainers []dockerTypes.Container + expectedJobs []ContainerStartJob + }{ + { + name: "No containers", + fakeContainers: []dockerTypes.Container{}, + expectedJobs: []ContainerStartJob{}, + }, + { + name: "One container without schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"no_schedule_1"}, + ID: "no_schedule_1", + }, + }, + expectedJobs: []ContainerStartJob{}, + }, + { + name: "One container with schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"has_schedule_1"}, + ID: "has_schedule_1", + Labels: map[string]string{ + "dockron.schedule": "* * * * *", + }, + }, + }, + expectedJobs: []ContainerStartJob{ + ContainerStartJob{ + Name: "has_schedule_1", + ContainerID: "has_schedule_1", + Schedule: "* * * * *", + Context: context.Background(), + Client: client, + }, + }, + }, + { + name: "One container with and one without schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"no_schedule_1"}, + ID: "no_schedule_1", + }, + dockerTypes.Container{ + Names: []string{"has_schedule_1"}, + ID: "has_schedule_1", + Labels: map[string]string{ + "dockron.schedule": "* * * * *", + }, + }, + }, + expectedJobs: []ContainerStartJob{ + ContainerStartJob{ + Name: "has_schedule_1", + ContainerID: "has_schedule_1", + Schedule: "* * * * *", + Context: context.Background(), + Client: client, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + log.Printf("Running %s", t.Name()) + + // Load fake containers + t.Logf("Fake containers: %+v", c.fakeContainers) + client.FakeContainers = c.fakeContainers + + jobs := QueryScheduledJobs(client) + + t.Logf("Expected jobs: %+v, Actual jobs: %+v", c.expectedJobs, jobs) + + ErrorUnequal(t, len(c.expectedJobs), len(jobs), "Job lengths don't match") + for i, job := range jobs { + ErrorUnequal(t, c.expectedJobs[i], job, "Job value does not match") + } + }) + } +} + +func TestScheduleJobs(t *testing.T) { + c := cron.New() + + t.Run("Schedule nothing", func(t *testing.T) { + log.Printf("Running %s", t.Name()) + jobs := []ContainerStartJob{} + ScheduleJobs(c, jobs) + + scheduledEntries := c.Entries() + + ErrorUnequal(t, len(jobs), len(scheduledEntries), "Job lengths don't match") + for i, job := range jobs { + // Set client for comparison + ErrorUnequal(t, job, scheduledEntries[i].Job, "Job value does not match") + } + }) + + t.Run("Schedule a job", func(t *testing.T) { + log.Printf("Running %s", t.Name()) + jobs := []ContainerStartJob{ + ContainerStartJob{ + ContainerID: "0123456789/has_schedule_1", + Name: "has_schedule_1", + Schedule: "* * * * *", + }, + } + ScheduleJobs(c, jobs) + + scheduledEntries := c.Entries() + + ErrorUnequal(t, len(jobs), len(scheduledEntries), "Job lengths don't match") + for i, job := range jobs { + // Set client for comparison + ErrorUnequal(t, job, scheduledEntries[i].Job, "Job value does not match") + } + }) + + // Subsequently scheduled jobs will append since we currently just stop and create a new cron + // Eventually this test case should change when proper removal is supported + + t.Run("Schedule a second job", func(t *testing.T) { + log.Printf("Running %s", t.Name()) + jobs := []ContainerStartJob{ + ContainerStartJob{ + ContainerID: "0123456789/has_schedule_2", + Name: "has_schedule_2", + Schedule: "* * * * *", + }, + } + ScheduleJobs(c, jobs) + jobs = append([]ContainerStartJob{ + ContainerStartJob{ + ContainerID: "0123456789/has_schedule_1", + Name: "has_schedule_1", + Schedule: "* * * * *", + }, + }, jobs...) + + scheduledEntries := c.Entries() + + ErrorUnequal(t, len(jobs), len(scheduledEntries), "Additional job didn't show") + for i, job := range jobs { + // Set client for comparison + ErrorUnequal(t, job, scheduledEntries[i].Job, "Job value does not match") + } + }) +} + +func TestDoLoop(t *testing.T) { + croner := cron.New() + client := NewFakeDockerClient() + + cases := []struct { + name string + fakeContainers []dockerTypes.Container + expectedJobs []ContainerStartJob + }{ + { + name: "No containers", + fakeContainers: []dockerTypes.Container{}, + expectedJobs: []ContainerStartJob{}, + }, + { + name: "One container without schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"no_schedule_1"}, + ID: "no_schedule_1", + }, + }, + expectedJobs: []ContainerStartJob{}, + }, + { + name: "One container with schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"has_schedule_1"}, + ID: "has_schedule_1", + Labels: map[string]string{ + "dockron.schedule": "* * * * *", + }, + }, + }, + expectedJobs: []ContainerStartJob{ + ContainerStartJob{ + Name: "has_schedule_1", + ContainerID: "has_schedule_1", + Schedule: "* * * * *", + Context: context.Background(), + Client: client, + }, + }, + }, + { + name: "One container with and one without schedule", + fakeContainers: []dockerTypes.Container{ + dockerTypes.Container{ + Names: []string{"no_schedule_1"}, + ID: "no_schedule_1", + }, + dockerTypes.Container{ + Names: []string{"has_schedule_1"}, + ID: "has_schedule_1", + Labels: map[string]string{ + "dockron.schedule": "* * * * *", + }, + }, + }, + expectedJobs: []ContainerStartJob{ + ContainerStartJob{ + Name: "has_schedule_1", + ContainerID: "has_schedule_1", + Schedule: "* * * * *", + Context: context.Background(), + Client: client, + }, + }, + }, + } + + for loopIndex, c := range cases { + t.Run(fmt.Sprintf("Loop %d: %s", loopIndex, c.name), func(t *testing.T) { + log.Printf("Running %s", t.Name()) + + // Load fake containers + t.Logf("Fake containers: %+v", c.fakeContainers) + client.FakeContainers = c.fakeContainers + + // Execute loop iteration loop + // This is in the for loop + croner := cron.New() + jobs := QueryScheduledJobs(client) + ScheduleJobs(croner, jobs) + + // Validate results + + scheduledEntries := croner.Entries() + t.Logf("Cron entries: %+v", scheduledEntries) + + ErrorUnequal(t, len(c.expectedJobs), len(scheduledEntries), "Job and entry lengths don't match") + for i, entry := range scheduledEntries { + ErrorUnequal(t, c.expectedJobs[i], entry.Job, "Job value does not match entry") + } + }) + } + + // Make sure the cron stops + croner.Stop() +}