From deb44660413d94ac5c40a01f6cfcd4632dab0d5d Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 23 Aug 2016 22:57:00 -0700 Subject: [PATCH] versioning: adds tooling to compile a program against a given old CNI version Allows us to write tests that cover interactions between components of differing versions --- pkg/version/testhelpers/testhelpers.go | 156 ++++++++++++++++++ .../testhelpers/testhelpers_suite_test.go | 27 +++ pkg/version/testhelpers/testhelpers_test.go | 106 ++++++++++++ test | 2 +- 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 pkg/version/testhelpers/testhelpers.go create mode 100644 pkg/version/testhelpers/testhelpers_suite_test.go create mode 100644 pkg/version/testhelpers/testhelpers_test.go diff --git a/pkg/version/testhelpers/testhelpers.go b/pkg/version/testhelpers/testhelpers.go new file mode 100644 index 00000000..773d0120 --- /dev/null +++ b/pkg/version/testhelpers/testhelpers.go @@ -0,0 +1,156 @@ +// Copyright 2016 CNI 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 testhelpers supports testing of CNI components of different versions +// +// For example, to build a plugin against an old version of the CNI library, +// we can pass the plugin's source and the old git commit reference to BuildAt. +// We could then test how the built binary responds when called by the latest +// version of this library. +package testhelpers + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const packageBaseName = "github.com/containernetworking/cni" + +func run(cmd *exec.Cmd) error { + out, err := cmd.CombinedOutput() + if err != nil { + command := strings.Join(cmd.Args, " ") + return fmt.Errorf("running %q: %s", command, out) + } + return nil +} + +func goBuildEnviron(gopath string) []string { + environ := os.Environ() + for i, kvp := range environ { + if strings.HasPrefix(kvp, "GOPATH=") { + environ[i] = "GOPATH=" + gopath + return environ + } + } + environ = append(environ, "GOPATH="+gopath) + return environ +} + +func buildGoProgram(gopath, packageName, outputFilePath string) error { + cmd := exec.Command("go", "build", "-o", outputFilePath, packageName) + cmd.Env = goBuildEnviron(gopath) + return run(cmd) +} + +func createSingleFilePackage(gopath, packageName string, fileContents []byte) error { + dirName := filepath.Join(gopath, "src", packageName) + err := os.MkdirAll(dirName, 0700) + if err != nil { + return err + } + + return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600) +} + +func removePackage(gopath, packageName string) error { + dirName := filepath.Join(gopath, "src", packageName) + return os.RemoveAll(dirName) +} + +func isRepoRoot(path string) bool { + _, err := ioutil.ReadDir(filepath.Join(path, ".git")) + return (err == nil) && (filepath.Base(path) == "cni") +} + +func LocateCurrentGitRepo() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for i := 0; i < 5; i++ { + if isRepoRoot(dir) { + return dir, nil + } + + dir, err = filepath.Abs(filepath.Dir(dir)) + if err != nil { + return "", fmt.Errorf("abs(dir(%q)): %s", dir, err) + } + } + + return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir) +} + +func gitCloneThisRepo(cloneDestination string) error { + err := os.MkdirAll(cloneDestination, 0700) + if err != nil { + return err + } + + currentGitRepo, err := LocateCurrentGitRepo() + if err != nil { + return err + } + + return run(exec.Command("git", "clone", currentGitRepo, cloneDestination)) +} + +func gitCheckout(localRepo string, gitRef string) error { + return run(exec.Command("git", "-C", localRepo, "checkout", gitRef)) +} + +// BuildAt builds the go programSource using the version of the CNI library +// at gitRef, and saves the resulting binary file at outputFilePath +func BuildAt(programSource []byte, gitRef string, outputFilePath string) error { + tempGoPath, err := ioutil.TempDir("", "cni-git-") + if err != nil { + return err + } + defer os.RemoveAll(tempGoPath) + + cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName) + err = gitCloneThisRepo(cloneDestination) + if err != nil { + return err + } + + err = gitCheckout(cloneDestination, gitRef) + if err != nil { + return err + } + + rand.Seed(time.Now().UnixNano()) + testPackageName := fmt.Sprintf("test-package-%x", rand.Int31()) + + err = createSingleFilePackage(tempGoPath, testPackageName, programSource) + if err != nil { + return err + } + defer removePackage(tempGoPath, testPackageName) + + err = buildGoProgram(tempGoPath, testPackageName, outputFilePath) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/version/testhelpers/testhelpers_suite_test.go b/pkg/version/testhelpers/testhelpers_suite_test.go new file mode 100644 index 00000000..72f65f9c --- /dev/null +++ b/pkg/version/testhelpers/testhelpers_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI 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 testhelpers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTesthelpers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testhelpers Suite") +} diff --git a/pkg/version/testhelpers/testhelpers_test.go b/pkg/version/testhelpers/testhelpers_test.go new file mode 100644 index 00000000..3473cd59 --- /dev/null +++ b/pkg/version/testhelpers/testhelpers_test.go @@ -0,0 +1,106 @@ +// Copyright 2016 CNI 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 testhelpers_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/containernetworking/cni/pkg/version/testhelpers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildAt", func() { + var ( + gitRef string + outputFilePath string + outputDir string + programSource []byte + ) + BeforeEach(func() { + programSource = []byte(`package main + +import "github.com/containernetworking/cni/pkg/skel" + +func c(_ *skel.CmdArgs) error { return nil } + +func main() { skel.PluginMain(c, c) } +`) + gitRef = "f4364185253" + + var err error + outputDir, err = ioutil.TempDir("", "bin") + Expect(err).NotTo(HaveOccurred()) + outputFilePath = filepath.Join(outputDir, "some-binary") + }) + + AfterEach(func() { + Expect(os.RemoveAll(outputDir)).To(Succeed()) + }) + + It("builds the provided source code using the CNI library at the given git ref", func() { + Expect(outputFilePath).NotTo(BeAnExistingFile()) + + err := testhelpers.BuildAt(programSource, gitRef, outputFilePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(outputFilePath).To(BeAnExistingFile()) + + cmd := exec.Command(outputFilePath) + cmd.Env = []string{"CNI_COMMAND=VERSION"} + output, err := cmd.CombinedOutput() + Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{})) + Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION")) + }) +}) + +var _ = Describe("LocateCurrentGitRepo", func() { + It("returns the path to the root of the CNI git repo", func() { + path, err := testhelpers.LocateCurrentGitRepo() + Expect(err).NotTo(HaveOccurred()) + + AssertItIsTheCNIRepoRoot(path) + }) + + Context("when run from a different directory", func() { + BeforeEach(func() { + os.Chdir("..") + }) + + It("still finds the CNI repo root", func() { + path, err := testhelpers.LocateCurrentGitRepo() + Expect(err).NotTo(HaveOccurred()) + + AssertItIsTheCNIRepoRoot(path) + }) + }) +}) + +func AssertItIsTheCNIRepoRoot(path string) { + Expect(path).To(BeADirectory()) + files, err := ioutil.ReadDir(path) + Expect(err).NotTo(HaveOccurred()) + + names := []string{} + for _, file := range files { + names = append(names, file.Name()) + } + + Expect(names).To(ContainElement("SPEC.md")) + Expect(names).To(ContainElement("libcni")) + Expect(names).To(ContainElement("cnitool")) +} diff --git a/test b/test index 7537b40f..8c2b3ce5 100755 --- a/test +++ b/test @@ -11,7 +11,7 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override