Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic integration tests #203

Merged
merged 14 commits into from Jun 10, 2019
Merged
3 changes: 2 additions & 1 deletion .travis.yml
Expand Up @@ -13,8 +13,9 @@ script:
- hack/verify-boilerplate.sh
- hack/verify-gofmt.sh
- hack/run-lint.sh
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
- go test -v -short -coverprofile=coverage.txt -covermode=atomic ./...
- hack/make-all.sh
- KREW_BINARY=$PWD/../out/bin/krew-linux_amd64 go test -v ./...
after_success:
- bash <(curl -s https://codecov.io/bash)
env:
Expand Down
52 changes: 52 additions & 0 deletions integration/integration_test.go
@@ -0,0 +1,52 @@
// Copyright 2019 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integration
ahmetb marked this conversation as resolved.
Show resolved Hide resolved

import (
"testing"

"sigs.k8s.io/krew/integration/krew"
)

const (
// validPlugin is a valid plugin with a small download size
validPlugin = "konfig"
)

func TestKrewInstall(t *testing.T) {
skipShort(t)

test, cleanup := krew.NewKrewTest(t)
defer cleanup()

test.WithIndex().Krew("install", validPlugin).RunOrFailOutput()
test.Call(validPlugin, "--help").RunOrFail()
}

ahmetb marked this conversation as resolved.
Show resolved Hide resolved
func TestKrewHelp(t *testing.T) {
skipShort(t)

test, cleanup := krew.NewKrewTest(t)
defer cleanup()

test.Krew("help").RunOrFail()
}

func skipShort(t *testing.T) {
t.Helper()
if testing.Short() {
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
t.Skip("skipping integration test")
}
}
99 changes: 99 additions & 0 deletions integration/krew/index.go
@@ -0,0 +1,99 @@
// Copyright 2019 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package krew

import (
"bytes"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sync"

"github.com/golang/glog"
"sigs.k8s.io/krew/cmd/krew/cmd"
)

const (
persistentIndexCache = "krew-persistent-index-cache"
)

var (
once sync.Once
indexTar []byte
)

// InitializeIndex initializes the krew index in `$root/index` with the actual krew-index.
// It caches the index tree as in-memory tar after the first run.
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
func (k *KrewTest) initializeIndex() {
once.Do(func() {
persistentCacheFile := filepath.Join(os.TempDir(), persistentIndexCache)
fileInfo, err := os.Stat(persistentCacheFile)

if err == nil && fileInfo.Mode().IsRegular() {
k.t.Logf("Using persistent index cache from file %q", persistentCacheFile)
if indexTar, err = ioutil.ReadFile(persistentCacheFile); err == nil {
return
}
}

if indexTar, err = initFromGitClone(); err != nil {
k.t.Fatalf("cannot clone repository: %s", err)
}

ioutil.WriteFile(persistentCacheFile, indexTar, 0600)
})

indexDir := filepath.Join(k.Root(), "index")
if err := os.Mkdir(indexDir, 0777); err != nil {
if os.IsExist(err) {
k.t.Log("initializeIndex should only be called once")
return
}
k.t.Fatal(err)
}

cmd := exec.Command("tar", "xzf", "-", "-C", indexDir)
cmd.Stdin = bytes.NewReader(indexTar)
if err := cmd.Run(); err != nil {
k.t.Fatalf("cannot restore index from cache: %s", err)
}
}

func initFromGitClone() ([]byte, error) {
const tarName = "index.tar"
indexRoot, err := ioutil.TempDir("", "krew-index-cache")
if err != nil {
return nil, err
}
defer func() {
err := os.RemoveAll(indexRoot)
glog.V(1).Infoln("cannot remove temporary directory:", err)
}()

cmd := exec.Command("git", "clone", "--depth=1", "--single-branch", "--no-tags", cmd.IndexURI)
cmd.Dir = indexRoot
if err = cmd.Run(); err != nil {
return nil, err
}

cmd = exec.Command("tar", "czf", tarName, "-C", "krew-index", ".")
cmd.Dir = indexRoot
if err = cmd.Run(); err != nil {
return nil, err
}

return ioutil.ReadFile(filepath.Join(indexRoot, tarName))
}
164 changes: 164 additions & 0 deletions integration/krew/krew.go
@@ -0,0 +1,164 @@
// Copyright 2019 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package krew

import (
"context"
"fmt"
"os"
"os/exec"
"testing"
"time"

"github.com/golang/glog"
"github.com/pkg/errors"
"sigs.k8s.io/krew/pkg/testutil"
)

const krewBinaryEnv = "KREW_BINARY"

// KrewTest is used to set up `krew` integration tests.
type KrewTest struct {
t *testing.T
plugin string
args []string
env []string
tempDir *testutil.TempDir
}

// NewKrewTest creates a fluent krew KrewTest.
func NewKrewTest(t *testing.T) (*KrewTest, func()) {
tempDir, cleanup := testutil.NewTempDir(t)
pathEnv := setupPathEnv(t, tempDir)
return &KrewTest{
t: t,
env: []string{pathEnv, fmt.Sprintf("KREW_ROOT=%s", tempDir.Root())},
tempDir: tempDir,
}, cleanup
}

func setupPathEnv(t *testing.T, tempDir *testutil.TempDir) string {
krewBinPath := tempDir.Path("bin")
if err := os.MkdirAll(krewBinPath, os.ModePerm); err != nil {
t.Fatal(err)
}

if krewBinary, found := os.LookupEnv(krewBinaryEnv); found {
if err := os.Symlink(krewBinary, tempDir.Path("bin/kubectl-krew")); err != nil {
t.Fatalf("Cannot link to krew: %s", err)
}
} else {
t.Logf("Environment variable %q was not found, using krew installation from host", krewBinaryEnv)
}

path, found := os.LookupEnv("PATH")
if !found {
t.Fatalf("PATH variable is not set up")
}

return fmt.Sprintf("PATH=%s:%s", krewBinPath, path)
}

// Call configures the runner to call plugin with arguments args.
func (k *KrewTest) Call(plugin string, args ...string) *KrewTest {
k.plugin = plugin
k.args = args
return k
}

// Krew configures the runner to call krew with arguments args.
func (k *KrewTest) Krew(args ...string) *KrewTest {
k.plugin = "krew"
k.args = args
return k
}

// Root returns the krew root directory for this test.
func (k *KrewTest) Root() string {
return k.tempDir.Root()
}

// WithIndex initializes the index with the actual krew-index from github/kubernetes-sigs/krew-index.
func (k *KrewTest) WithIndex() *KrewTest {
k.initializeIndex()
return k
}

// WithEnv sets an environment variable for the krew run.
func (k *KrewTest) WithEnv(key string, value interface{}) *KrewTest {
if key == "KREW_ROOT" {
glog.V(1).Infoln("Overriding KREW_ROOT in tests is forbidden")
return k
}
k.env = append(k.env, fmt.Sprintf("%s=%v", key, value))
return k
}

// RunOrFail runs the krew command and fails the test if the command returns an error.
func (k *KrewTest) RunOrFail() {
k.t.Helper()
if err := k.Run(); err != nil {
k.t.Fatal(err)
}
}

// Run runs the krew command.
func (k *KrewTest) Run() error {
k.t.Helper()

cmd := k.cmd(context.Background())
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
glog.V(1).Infoln(cmd.Args)

start := time.Now()
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "krew %v", k.args)
}

glog.V(1).Infoln("Ran in", time.Since(start))
return nil
}

// RunOrFailOutput runs the krew command and fails the test if the command
// returns an error. It only returns the standard output.
func (k *KrewTest) RunOrFailOutput() []byte {
k.t.Helper()

cmd := k.cmd(context.Background())
glog.V(1).Infoln(cmd.Args)

start := time.Now()
out, err := cmd.CombinedOutput()
if err != nil {
k.t.Fatalf("krew %v: %v, %s", k.args, err, out)
}

glog.V(1).Infoln("Ran in", time.Since(start))
return out
}

func (k *KrewTest) cmd(ctx context.Context) *exec.Cmd {
args := make([]string, 0, len(k.args)+1)
args = append(args, k.plugin)
args = append(args, k.args...)

cmd := exec.CommandContext(ctx, "kubectl", args...)
cmd.Env = append(os.Environ(), k.env...)

return cmd
}

func (k *KrewTest) TempDir() *testutil.TempDir {
return k.tempDir
}
15 changes: 15 additions & 0 deletions pkg/testutil/tempdir.go
Expand Up @@ -70,3 +70,18 @@ func (td *TempDir) Write(file string, content []byte) *TempDir {
}
return td
}

// List lists all the files in the subpath of the temp directory.
// The input path is expected to use '/' as directory separator regardless of the host OS.
func (td *TempDir) List(path string) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems redundant now. feel free to remove.

var files []string

err := filepath.Walk(td.Path(path), func(path string, info os.FileInfo, err error) error {
if err == nil && info.Mode().IsRegular() {
files = append(files, filepath.ToSlash(path))
}
return err
})

return files, err
}