Skip to content

Commit

Permalink
Add tests for Parallel and Sorted runners (#21)
Browse files Browse the repository at this point in the history
* test examples

* fix

* fix

* fix

* fix

* fix

* fix

* added dependencies

* added tests for Parallel and Sorted runners

* test linter as well

* fix linter

* fix build status badge

* make linter happy

* make containers self-sufficient

* improve caching of docker image

* revert

* update linter

* add more margin because travis tests in macos are slower

* travis is too unstable

* use special _test package for testing

* rename jobResult to jr for brevity

* added testHosts to test functions

* moved helpers to testing.go and copied nullloger

* remove runner_test

* use "vendored" null logger

* clarity

* rename serial.go to sorted.go

* fixes

* forgot to include :S

* increase margin of error
  • Loading branch information
dbarrosop authored and nleiva committed Jun 30, 2019
1 parent e7fb4a9 commit 12c7c0a
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 6 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
PROJECT="github.com/nornir-automation/gornir"
GOLANGCI_LINT_VER="v1.17"

.PHONY: tests
tests:
Expand All @@ -12,7 +13,7 @@ lint:
-w /go/src/$(PROJECT) \
-e GO111MODULE=on \
-e GOPROXY=https://proxy.golang.org \
golangci/golangci-lint \
golangci/golangci-lint:$(GOLANGCI_LINT_VER) \
golangci-lint run

.PHONY: test-suite
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugins/runner/parallel.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ type ParallelRunner struct {
wg *sync.WaitGroup
}

// Parallel returns an instantiated ParallelRunner
func Parallel() *ParallelRunner {
return &ParallelRunner{
wg: &sync.WaitGroup{},
}
}

// Run implements the Run method of the gornir.Runner interface
func (r ParallelRunner) Run(ctx context.Context, task gornir.Task, hosts map[string]*gornir.Host, jp *gornir.JobParameters, results chan *gornir.JobResult) error {
logger := jp.Logger().WithField("runner", "Parallel")
logger.Debug("starting runner")
Expand All @@ -35,11 +37,13 @@ func (r ParallelRunner) Run(ctx context.Context, task gornir.Task, hosts map[str
return nil
}

// Wait implements the Wait method of the gornir.Runner interface
func (r ParallelRunner) Wait() error {
r.wg.Wait()
return nil
}

// Close implements the Close method of the gornir.Runner interface
func (r ParallelRunner) Close() error {
return nil
}
75 changes: 75 additions & 0 deletions pkg/plugins/runner/parallel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package runner_test

import (
"context"
"testing"
"time"

"github.com/google/go-cmp/cmp"

"github.com/nornir-automation/gornir/pkg/gornir"
"github.com/nornir-automation/gornir/pkg/plugins/runner"
)

// TestParallel is going to check that the func runs on all hosts
// and that it takes less than X time to complete. The test func
// basically will sleep for N ms and given we are using goroutines
// the completion should only be slightly above it even though
// we are sleeping once per device
func TestParallel(t *testing.T) {
testCases := []struct {
name string
expected map[string]bool
sleepDuration time.Duration
}{
{
name: "simple test",
expected: map[string]bool{"dev1": true, "dev2": true, "dev3": true, "dev4": true},
sleepDuration: 200 * time.Millisecond,
},
}

testHosts := map[string]*gornir.Host{
"dev1": {Hostname: "dev1"},
"dev2": {Hostname: "dev2"},
"dev3": {Hostname: "dev3"},
"dev4": {Hostname: "dev4"},
}

for _, tc := range testCases {
tc := tc
results := make(chan *gornir.JobResult, len(testHosts))
t.Run(tc.name, func(t *testing.T) {
rnr := runner.Parallel()
startTime := time.Now()
if err := rnr.Run(
context.Background(),
&testTaskSleep{sleepDuration: tc.sleepDuration},
testHosts,
gornir.NewJobParameters("test", NewNullLogger()),
results,
); err != nil {
t.Fatal(err)
}
if err := rnr.Wait(); err != nil {
t.Fatal(err)
}
close(results)

// let's process the results and turn it into a map so we can
// compare with our expected value
got := make(map[string]bool)
for res := range results {
got[res.JobParameters().Host().Hostname] = res.Data().(*testTaskSleepResults).success
}
if !cmp.Equal(got, tc.expected) {
t.Error(cmp.Diff(got, tc.expected))
}
// now we check test took what we expected
if time.Since(startTime) > (tc.sleepDuration + time.Millisecond*100) {
t.Errorf("test took to long, parallelization might not be working: %v\n", time.Since(startTime).Seconds())
}
})
}

}
59 changes: 59 additions & 0 deletions pkg/plugins/runner/running_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package runner_test

import (
"context"
"sync"
"time"

"github.com/nornir-automation/gornir/pkg/gornir"
)

type testTaskSleep struct {
sleepDuration time.Duration
}

type testTaskSleepResults struct {
success bool
}

func (t *testTaskSleep) Run(ctx context.Context, wg *sync.WaitGroup, jp *gornir.JobParameters, jr chan *gornir.JobResult) {
defer wg.Done()
time.Sleep(t.sleepDuration)
result := gornir.NewJobResult(ctx, jp)
result.SetData(&testTaskSleepResults{success: true})
jr <- result
}

// Null is a logger that doesn't do anything. Implements gornir.Logger interface
type Null struct {
}

// NewNullLogger instantiates a new Null logger
func NewNullLogger() *Null {
return &Null{}
}

// WithField implements gornir.Logger interface
func (n *Null) WithField(field string, value interface{}) gornir.Logger {
return n
}

// Info implements gornir.Logger interface
func (n *Null) Info(args ...interface{}) {
}

// Debug implements gornir.Logger interface
func (n *Null) Debug(args ...interface{}) {
}

// Error implements gornir.Logger interface
func (n *Null) Error(args ...interface{}) {
}

// Warn implements gornir.Logger interface
func (n *Null) Warn(args ...interface{}) {
}

// Fatal implements gornir.Logger interface
func (n *Null) Fatal(args ...interface{}) {
}
4 changes: 4 additions & 0 deletions pkg/plugins/runner/serial.go → pkg/plugins/runner/sorted.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
type SortedRunner struct {
}

// Sorted returns an instantiated SortedRunner
func Sorted() *SortedRunner {
return &SortedRunner{}
}

// Run implements the Run method of the gornir.Runner interface
func (r SortedRunner) Run(ctx context.Context, task gornir.Task, hosts map[string]*gornir.Host, jp *gornir.JobParameters, results chan *gornir.JobResult) error {
logger := jp.Logger().WithField("runner", "Sorted")
logger.Debug("starting runner")
Expand Down Expand Up @@ -44,10 +46,12 @@ func (r SortedRunner) Run(ctx context.Context, task gornir.Task, hosts map[strin
return nil
}

// Wait implements the Wait method of the gornir.Runner interface
func (r SortedRunner) Wait() error {
return nil
}

// Close implements the Close method of the gornir.Runner interface
func (r SortedRunner) Close() error {
return nil
}
70 changes: 70 additions & 0 deletions pkg/plugins/runner/sorted_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package runner_test

import (
"context"
"testing"
"time"

"github.com/google/go-cmp/cmp"

"github.com/nornir-automation/gornir/pkg/gornir"
"github.com/nornir-automation/gornir/pkg/plugins/runner"
)

// TestSorted runs test func and verifies the hosts are executed
// in alphabetical order by checking the results come in the right
// order
func TestSorted(t *testing.T) {
testCases := []struct {
name string
expected []string
sleepDuration time.Duration
}{
{
name: "run in alphabetical order",
expected: []string{"dev1", "dev2", "dev3", "dev4"},
sleepDuration: 1 * time.Millisecond,
},
}

testHosts := map[string]*gornir.Host{
"dev1": {Hostname: "dev1"},
"dev2": {Hostname: "dev2"},
"dev3": {Hostname: "dev3"},
"dev4": {Hostname: "dev4"},
}

for _, tc := range testCases {
tc := tc
results := make(chan *gornir.JobResult, len(testHosts))
t.Run(tc.name, func(t *testing.T) {
rnr := runner.Sorted()
if err := rnr.Run(
context.Background(),
&testTaskSleep{sleepDuration: tc.sleepDuration},
testHosts,
gornir.NewJobParameters("test", NewNullLogger()),
results,
); err != nil {
t.Fatal(err)
}
if err := rnr.Wait(); err != nil {
t.Fatal(err)
}
close(results)

// let's process the results and turn it into a list so we can
// compare with our expected value
got := make([]string, len(testHosts))
i := 0
for res := range results {
got[i] = res.JobParameters().Host().Hostname
i++
}
if !cmp.Equal(got, tc.expected) {
t.Error(cmp.Diff(got, tc.expected))
}
})
}

}
10 changes: 5 additions & 5 deletions pkg/plugins/task/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type RemoteCommandResults struct {
Stderr []byte // Stderr written by the command
}

func (r *RemoteCommand) Run(ctx context.Context, wg *sync.WaitGroup, jp *gornir.JobParameters, jobResult chan *gornir.JobResult) {
func (r *RemoteCommand) Run(ctx context.Context, wg *sync.WaitGroup, jp *gornir.JobParameters, jr chan *gornir.JobResult) {
defer wg.Done()
host := jp.Host()
result := gornir.NewJobResult(ctx, jp)
Expand All @@ -42,14 +42,14 @@ func (r *RemoteCommand) Run(ctx context.Context, wg *sync.WaitGroup, jp *gornir.
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host.Hostname, port), sshConfig)
if err != nil {
result.SetErr(errors.Wrap(err, "failed to dial"))
jobResult <- result
jr <- result
return
}

session, err := client.NewSession()
if err != nil {
result.SetErr(errors.Wrap(err, "failed to create session"))
jobResult <- result
jr <- result
return
}
defer session.Close()
Expand All @@ -61,9 +61,9 @@ func (r *RemoteCommand) Run(ctx context.Context, wg *sync.WaitGroup, jp *gornir.

if err := session.Run(r.Command); err != nil {
result.SetErr(errors.Wrap(err, "failed to execute command"))
jobResult <- result
jr <- result
return
}
result.SetData(&RemoteCommandResults{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()})
jobResult <- result
jr <- result
}

0 comments on commit 12c7c0a

Please sign in to comment.