diff --git a/Makefile b/Makefile index a9ca1e5..23594d9 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ test: fmt vet test-unit test-unit: go test -race -coverprofile=coverage.txt -covermode=atomic ./... +.PHONY: test-e2e +test-e2e: build + go test -v ./pkg/e2e/e2e_test.go + # Make sure go.mod and go.sum are not modified .PHONY: test-dirty test-dirty: build diff --git a/README.md b/README.md index 3f2a1ca..1e339c4 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,21 @@ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details. $ make build ``` +### End To End Testing + +The end to end test can be run by simply running `make test-e2e`. +To run the test, you need to have the [consul binary](https://releases.hashicorp.com/consul/) available in your path. +It simulates a create and update of consul KV pairs and confirms every operation is successful. +The test data is stored within this repo so developers do not have to setup an external repo to test. + +The tests can manually be run by starting Consul in dev mode, and then manually running `git2consul` with one of the config files provided. +For example: +``` +$ consul agent -dev +$ # In a separate terminal +$ ./git2consul -config pkg/e2e/data/create-config.json -once -debug +``` + ### Releases This project is using [goreleaser](https://goreleaser.com). GitHub release creation is automated using Travis CI. New releases are automatically created when new tags are pushed to the repo. diff --git a/pkg/e2e/data/create-config.json b/pkg/e2e/data/create-config.json new file mode 100644 index 0000000..1169379 --- /dev/null +++ b/pkg/e2e/data/create-config.json @@ -0,0 +1,12 @@ +{ + "repos": [ + { + "name": "e2e", + "url": "http://github.com/KohlsTechnology/git2consul-go.git", + "branches": [ + "master" + ], + "source_root": "/pkg/e2e/data/create/" + } + ] +} diff --git a/pkg/e2e/data/create/artist b/pkg/e2e/data/create/artist new file mode 100644 index 0000000..715c91e --- /dev/null +++ b/pkg/e2e/data/create/artist @@ -0,0 +1 @@ +mozart diff --git a/pkg/e2e/data/create/genre b/pkg/e2e/data/create/genre new file mode 100644 index 0000000..c96a171 --- /dev/null +++ b/pkg/e2e/data/create/genre @@ -0,0 +1 @@ +classical diff --git a/pkg/e2e/data/delete-config.json b/pkg/e2e/data/delete-config.json new file mode 100644 index 0000000..67bfcd4 --- /dev/null +++ b/pkg/e2e/data/delete-config.json @@ -0,0 +1,12 @@ +{ + "repos": [ + { + "name": "e2e", + "url": "http://github.com/KohlsTechnology/git2consul-go.git", + "branches": [ + "master" + ], + "source_root": "/pkg/e2e/data/delete/" + } + ] +} diff --git a/pkg/e2e/data/delete/artist b/pkg/e2e/data/delete/artist new file mode 100644 index 0000000..715c91e --- /dev/null +++ b/pkg/e2e/data/delete/artist @@ -0,0 +1 @@ +mozart diff --git a/pkg/e2e/data/update-config.json b/pkg/e2e/data/update-config.json new file mode 100644 index 0000000..28754d2 --- /dev/null +++ b/pkg/e2e/data/update-config.json @@ -0,0 +1,12 @@ +{ + "repos": [ + { + "name": "e2e", + "url": "http://github.com/KohlsTechnology/git2consul-go.git", + "branches": [ + "master" + ], + "source_root": "/pkg/e2e/data/update/" + } + ] +} diff --git a/pkg/e2e/data/update/artist b/pkg/e2e/data/update/artist new file mode 100644 index 0000000..53e48b9 --- /dev/null +++ b/pkg/e2e/data/update/artist @@ -0,0 +1 @@ +beethoven diff --git a/pkg/e2e/data/update/genre b/pkg/e2e/data/update/genre new file mode 100644 index 0000000..c96a171 --- /dev/null +++ b/pkg/e2e/data/update/genre @@ -0,0 +1 @@ +classical diff --git a/pkg/e2e/e2e_test.go b/pkg/e2e/e2e_test.go new file mode 100644 index 0000000..0f3d7e2 --- /dev/null +++ b/pkg/e2e/e2e_test.go @@ -0,0 +1,179 @@ +// +build e2e + +package e2e + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/hashicorp/consul/api" +) + +func TestE2E(t *testing.T) { + // Setup local Consul test server + consulCmd := exec.Command("consul", "agent", "-dev") + consulPipeReader, consulPipeWriter := io.Pipe() + consulScanner := bufio.NewScanner(consulPipeReader) + consulTee := io.MultiWriter(consulPipeWriter, os.Stderr) // dumping the output on Stderr can be useful when debugging this test + + consulCmd.Stdout = consulTee + consulCmd.Stderr = consulTee + if err := consulCmd.Start(); err != nil { + t.Fatalf("failed to start local consul server: %s", err) + } + defer consulCmd.Process.Signal(syscall.SIGTERM) + + consulDone := make(chan error, 0) + go func() { + consulDone <- consulCmd.Wait() + }() + + err := waitForString(consulScanner, 20*time.Second, "==> Consul agent running!") + if err != nil { + t.Fatal("initialization of consul server failed", err) + } + t.Log("initialization of consul server finished") + + client, err := api.NewClient(api.DefaultConfig()) + if err != nil { + t.Fatal("failed to initialize consul api client", err) + } + t.Log("initilization of consul api finished") + kv := client.KV() + + projectDir, err := GetRootProjectDir() + if err != nil { + t.Fatal("failed to get working project directory", err) + } + + g2cCmd := exec.Command(projectDir+"/git2consul", + "-config", + projectDir+"/pkg/e2e/data/create-config.json", + "-debug", + "-once") + err = executeCommand(g2cCmd, "Terminating git2consul") + if err != nil { + t.Fatal("git2consul run failed", err) + } else { + t.Log("git2consul ran successfully") + } + + pair, _, err := kv.Get("e2e/master/genre", nil) + if err != nil { + t.Fatal("failed to get pair e2e/master/genre", err) + } + if pair == nil { + t.Fatal("did not find expected pair at e2e/master/genre") + } + if strings.TrimSpace(string(pair.Value)) != "classical" { + t.Errorf("got %s want %s", string(pair.Value), "classical") + } else { + t.Log("confirmed e2e/master/genre was properly set") + } + + // TODO: remove delete when issue #31 is resolved this is a workaround because if + // the ref matches the current commit, the kv pairs will not be updated + _, err = kv.Delete("e2e/master.ref", nil) + if err != nil { + t.Fatal("failed to delete git reference") + } + + g2cCmd = exec.Command(projectDir+"/git2consul", + "-config", + projectDir+"/pkg/e2e/data/update-config.json", + "-debug", + "-once") + err = executeCommand(g2cCmd, "Terminating git2consul") + if err != nil { + t.Fatal("git2consul run failed", err) + } else { + t.Log("git2consul ran successfully") + } + + pair, _, err = kv.Get("e2e/master/artist", nil) + if err != nil { + t.Fatal("failed to get pair e2e/master/artist", err) + } + if pair == nil { + t.Fatal("did not find expected pair at e2e/master/artist") + } + if strings.TrimSpace(string(pair.Value)) != "beethoven" { + t.Errorf("got %s want %s", string(pair.Value), "beethoven") + } else { + t.Log("confirmed e2e/master/genre was properly updated") + } + + // TODO: add stage to simulate delete of kv pair +} + +func executeCommand(command *exec.Cmd, expectedLog string) (err error) { + commandPipeReader, commandPipeWriter := io.Pipe() + + commandScanner := bufio.NewScanner(commandPipeReader) + commandTee := io.MultiWriter(commandPipeWriter, os.Stderr) // dumping the output on Stderr can be useful when debugging this test + + command.Stdout = commandTee + command.Stderr = commandTee + if err := command.Start(); err != nil { + return err + } + defer command.Process.Signal(syscall.SIGTERM) + + commandDone := make(chan error, 0) + go func() { + commandDone <- command.Wait() + }() + + err = waitForString(commandScanner, 20*time.Second, expectedLog) + if err != nil { + return err + } + + return nil +} + +func waitForString(s *bufio.Scanner, timeout time.Duration, want string) error { + done := make(chan error) + go func() { + for s.Scan() { + if strings.Contains(s.Text(), want) { + done <- nil + return + } + } + if s.Err() != nil { + done <- s.Err() + } + done <- fmt.Errorf("process finished without printing expected string %q", want) + }() + select { + case err := <-done: + return err + case <-time.After(timeout): + return fmt.Errorf("wait for string %q timed out", want) + } +} + +// GetRootProjectDir returns path to root directory of project +func GetRootProjectDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + for !strings.HasSuffix(wd, "git2consul-go") { + if wd == "/" { + return "", errors.New(`cannot find project directory, "/" reached`) + } + wd = filepath.Dir(wd) + } + return wd, nil +}