From ecdc5cc2f2ff07543e241b4492fa90ec280d2fb2 Mon Sep 17 00:00:00 2001 From: Ryan Moran Date: Wed, 11 Dec 2019 14:18:30 -0500 Subject: [PATCH] Adds jam cli for building buildpacks [#170138077] Co-authored-by: Forest Eckhardt Co-authored-by: Daniel Thornton --- build_test.go | 1 + cargo/buildpack_parser.go | 45 ++++ cargo/buildpack_parser_test.go | 91 ++++++++ cargo/fakes/executable.go | 34 +++ cargo/file_bundler.go | 57 +++++ cargo/file_bundler_test.go | 90 ++++++++ cargo/init_test.go | 48 ++++ cargo/jam/commands/fakes/config_parser.go | 33 +++ cargo/jam/commands/fakes/file_bundler.go | 37 ++++ cargo/jam/commands/fakes/prepackager.go | 30 +++ cargo/jam/commands/fakes/tar_builder.go | 34 +++ cargo/jam/commands/init_test.go | 52 +++++ cargo/jam/commands/pack.go | 104 +++++++++ cargo/jam/commands/pack_test.go | 206 ++++++++++++++++++ cargo/jam/errors_test.go | 51 +++++ cargo/jam/init_test.go | 79 +++++++ cargo/jam/main.go | 39 ++++ cargo/jam/pack_test.go | 100 +++++++++ cargo/jam/testdata/example-cnb/bin/build | 1 + cargo/jam/testdata/example-cnb/bin/detect | 1 + cargo/jam/testdata/example-cnb/buildpack.toml | 10 + .../jam/testdata/example-cnb/scripts/build.sh | 4 + cargo/pre_packager.go | 31 +++ cargo/pre_packager_test.go | 44 ++++ cargo/tar_builder.go | 57 +++++ cargo/tar_builder_test.go | 121 ++++++++++ detect_test.go | 1 + go.mod | 13 +- go.sum | 41 +++- init_test.go | 58 ----- internal/init_test.go | 58 ----- internal/toml_writer_test.go | 3 +- matchers/match_toml.go | 63 ++++++ 33 files changed, 1511 insertions(+), 126 deletions(-) create mode 100644 cargo/buildpack_parser.go create mode 100644 cargo/buildpack_parser_test.go create mode 100644 cargo/fakes/executable.go create mode 100644 cargo/file_bundler.go create mode 100644 cargo/file_bundler_test.go create mode 100644 cargo/jam/commands/fakes/config_parser.go create mode 100644 cargo/jam/commands/fakes/file_bundler.go create mode 100644 cargo/jam/commands/fakes/prepackager.go create mode 100644 cargo/jam/commands/fakes/tar_builder.go create mode 100644 cargo/jam/commands/init_test.go create mode 100644 cargo/jam/commands/pack.go create mode 100644 cargo/jam/commands/pack_test.go create mode 100644 cargo/jam/errors_test.go create mode 100644 cargo/jam/init_test.go create mode 100644 cargo/jam/main.go create mode 100644 cargo/jam/pack_test.go create mode 100755 cargo/jam/testdata/example-cnb/bin/build create mode 100755 cargo/jam/testdata/example-cnb/bin/detect create mode 100644 cargo/jam/testdata/example-cnb/buildpack.toml create mode 100755 cargo/jam/testdata/example-cnb/scripts/build.sh create mode 100644 cargo/pre_packager.go create mode 100644 cargo/pre_packager_test.go create mode 100644 cargo/tar_builder.go create mode 100644 cargo/tar_builder_test.go create mode 100644 matchers/match_toml.go diff --git a/build_test.go b/build_test.go index ff6cac43..8ca3c65c 100644 --- a/build_test.go +++ b/build_test.go @@ -12,6 +12,7 @@ import ( "github.com/cloudfoundry/packit/fakes" "github.com/sclevine/spec" + . "github.com/cloudfoundry/packit/matchers" . "github.com/onsi/gomega" ) diff --git a/cargo/buildpack_parser.go b/cargo/buildpack_parser.go new file mode 100644 index 00000000..9ea47d81 --- /dev/null +++ b/cargo/buildpack_parser.go @@ -0,0 +1,45 @@ +package cargo + +import ( + "os" + + "github.com/pelletier/go-toml" +) + +type Config struct { + API string `toml:"api"` + Buildpack ConfigBuildpack `toml:"buildpack"` + Metadata ConfigMetadata `toml:"metadata"` +} + +type ConfigBuildpack struct { + ID string `toml:"id"` + Name string `toml:"name"` + Version string `toml:"version"` +} + +type ConfigMetadata struct { + IncludeFiles []string `toml:"include_files"` + PrePackage string `toml:"pre_package"` +} + +type BuildpackParser struct{} + +func NewBuildpackParser() BuildpackParser { + return BuildpackParser{} +} + +func (p BuildpackParser) Parse(path string) (Config, error) { + file, err := os.Open(path) + if err != nil { + return Config{}, err + } + + var config Config + err = toml.NewDecoder(file).Decode(&config) + if err != nil { + return Config{}, err + } + + return config, nil +} diff --git a/cargo/buildpack_parser_test.go b/cargo/buildpack_parser_test.go new file mode 100644 index 00000000..70a12ebb --- /dev/null +++ b/cargo/buildpack_parser_test.go @@ -0,0 +1,91 @@ +package cargo_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/cloudfoundry/packit/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testBuildpackParser(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + path string + parser cargo.BuildpackParser + ) + + it.Before(func() { + file, err := ioutil.TempFile("", "buildpack.toml") + Expect(err).NotTo(HaveOccurred()) + + _, err = file.WriteString(`api = "0.2" +[buildpack] +id = "some-buildpack-id" +name = "some-buildpack-name" +version = "some-buildpack-version" + +[metadata] +include_files = ["some-include-file", "other-include-file"] +pre_package = "some-pre-package-script.sh"`) + Expect(err).NotTo(HaveOccurred()) + + Expect(file.Close()).To(Succeed()) + + path = file.Name() + + parser = cargo.NewBuildpackParser() + }) + + it.After(func() { + Expect(os.RemoveAll(path)).To(Succeed()) + }) + + context("Parse", func() { + it("parses a given buildpack.toml", func() { + config, err := parser.Parse(path) + Expect(err).NotTo(HaveOccurred()) + Expect(config).To(Equal(cargo.Config{ + API: "0.2", + Buildpack: cargo.ConfigBuildpack{ + ID: "some-buildpack-id", + Name: "some-buildpack-name", + Version: "some-buildpack-version", + }, + Metadata: cargo.ConfigMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + }, + })) + }) + + context("when the buildpack.toml does not exist", func() { + it.Before(func() { + Expect(os.Remove(path)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) + }) + }) + + context("when the buildpack.toml is malformed", func() { + it.Before(func() { + Expect(ioutil.WriteFile(path, []byte("%%%"), 0644)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("keys cannot contain % character"))) + }) + }) + }) +} diff --git a/cargo/fakes/executable.go b/cargo/fakes/executable.go new file mode 100644 index 00000000..3343ff6b --- /dev/null +++ b/cargo/fakes/executable.go @@ -0,0 +1,34 @@ +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/packit/pexec" +) + +type Executable struct { + ExecuteCall struct { + sync.Mutex + CallCount int + Receives struct { + Execution pexec.Execution + } + Returns struct { + StdOut string + StdError string + Err error + } + Stub func(pexec.Execution) (string, string, error) + } +} + +func (f *Executable) Execute(param1 pexec.Execution) (string, string, error) { + f.ExecuteCall.Lock() + defer f.ExecuteCall.Unlock() + f.ExecuteCall.CallCount++ + f.ExecuteCall.Receives.Execution = param1 + if f.ExecuteCall.Stub != nil { + return f.ExecuteCall.Stub(param1) + } + return f.ExecuteCall.Returns.StdOut, f.ExecuteCall.Returns.StdError, f.ExecuteCall.Returns.Err +} diff --git a/cargo/file_bundler.go b/cargo/file_bundler.go new file mode 100644 index 00000000..976d93db --- /dev/null +++ b/cargo/file_bundler.go @@ -0,0 +1,57 @@ +package cargo + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pelletier/go-toml" +) + +type FileBundler struct{} + +func NewFileBundler() FileBundler { + return FileBundler{} +} + +func (b FileBundler) Bundle(root string, paths []string, config Config) ([]File, error) { + var files []File + for _, path := range paths { + file := File{Name: path} + + switch path { + case "buildpack.toml": + buf := bytes.NewBuffer(nil) + err := toml.NewEncoder(buf).Encode(config) + if err != nil { + return nil, fmt.Errorf("error encoding buildpack.toml: %s", err) + } + + file.ReadCloser = ioutil.NopCloser(buf) + file.Size = int64(buf.Len()) + file.Mode = int64(0644) + + default: + fd, err := os.Open(filepath.Join(root, path)) + if err != nil { + return nil, fmt.Errorf("error opening included file: %s", err) + } + + info, err := fd.Stat() + if err != nil { + return nil, fmt.Errorf("error stating included file: %s", err) + } + + file.ReadCloser = fd + file.Size = info.Size() + file.Mode = int64(info.Mode()) + + } + + files = append(files, file) + } + + return files, nil +} diff --git a/cargo/file_bundler_test.go b/cargo/file_bundler_test.go new file mode 100644 index 00000000..4f28945c --- /dev/null +++ b/cargo/file_bundler_test.go @@ -0,0 +1,90 @@ +package cargo_test + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/cloudfoundry/packit/cargo" + "github.com/sclevine/spec" + + . "github.com/cloudfoundry/packit/matchers" + . "github.com/onsi/gomega" +) + +func testFileBundler(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + fileBundler cargo.FileBundler + ) + + it.Before(func() { + fileBundler = cargo.NewFileBundler() + }) + + context("Bundle", func() { + it("returns a list of cargo files", func() { + files, err := fileBundler.Bundle(filepath.Join("jam", "testdata", "example-cnb"), []string{"bin/build", "bin/detect", "buildpack.toml"}, cargo.Config{ + API: "0.2", + Buildpack: cargo.ConfigBuildpack{ + ID: "other-buildpack-id", + Name: "other-buildpack-name", + Version: "other-buildpack-version", + }, + Metadata: cargo.ConfigMetadata{ + IncludeFiles: []string{ + "bin/build", + "bin/detect", + "buildpack.toml", + }, + PrePackage: "some-pre-package-script.sh", + }, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(files).To(HaveLen(3)) + + Expect(files[0].Name).To(Equal("bin/build")) + Expect(files[0].Size).To(Equal(int64(14))) + Expect(files[0].Mode).To(Equal(int64(0755))) + + content, err := ioutil.ReadAll(files[0]) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(Equal("build-contents")) + + Expect(files[1].Name).To(Equal("bin/detect")) + Expect(files[1].Size).To(Equal(int64(15))) + Expect(files[1].Mode).To(Equal(int64(0755))) + + content, err = ioutil.ReadAll(files[1]) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(Equal("detect-contents")) + + Expect(files[2].Name).To(Equal("buildpack.toml")) + Expect(files[2].Size).To(Equal(int64(242))) + Expect(files[2].Mode).To(Equal(int64(0644))) + + content, err = ioutil.ReadAll(files[2]) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(MatchTOML(`api = "0.2" +[buildpack] +id = "other-buildpack-id" +name = "other-buildpack-name" +version = "other-buildpack-version" + +[metadata] +include_files = ["bin/build", "bin/detect", "buildpack.toml"] +pre_package = "some-pre-package-script.sh"`)) + }) + + context("error cases", func() { + context("when included file does not exist", func() { + it("fails", func() { + _, err := fileBundler.Bundle(filepath.Join("jam", "testdata", "example-cnb"), []string{"bin/fake/build", "bin/detect", "buildpack.toml"}, cargo.Config{}) + Expect(err).To(MatchError(ContainSubstring("error opening included file:"))) + }) + }) + }) + }) +} diff --git a/cargo/init_test.go b/cargo/init_test.go index 3823638e..cebe824b 100644 --- a/cargo/init_test.go +++ b/cargo/init_test.go @@ -1,6 +1,12 @@ package cargo_test import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" "testing" "github.com/sclevine/spec" @@ -9,7 +15,49 @@ import ( func TestUnitCargo(t *testing.T) { suite := spec.New("cargo", spec.Report(report.Terminal{})) + suite("BuildpackParser", testBuildpackParser) + suite("FileBundler", testFileBundler) + suite("TarBuilder", testTarBuilder) suite("Transport", testTransport) suite("ValidatedReader", testValidatedReader) + suite("PrePackager", testPrePackager) suite.Run(t) } + +func ExtractFile(file *os.File, name string) ([]byte, *tar.Header, error) { + _, err := file.Seek(0, 0) + if err != nil { + return nil, nil, err + } + + //TODO: Replace me with decompression library + gzr, err := gzip.NewReader(file) + if err != nil { + return nil, nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + hdr, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, nil, err + } + + if hdr.Name == name { + contents, err := ioutil.ReadAll(tr) + if err != nil { + return nil, nil, err + } + + return contents, hdr, nil + } + + } + + return nil, nil, fmt.Errorf("no such file: %s", name) +} diff --git a/cargo/jam/commands/fakes/config_parser.go b/cargo/jam/commands/fakes/config_parser.go new file mode 100644 index 00000000..576ed2a3 --- /dev/null +++ b/cargo/jam/commands/fakes/config_parser.go @@ -0,0 +1,33 @@ +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/packit/cargo" +) + +type ConfigParser struct { + ParseCall struct { + sync.Mutex + CallCount int + Receives struct { + Path string + } + Returns struct { + Config cargo.Config + Error error + } + Stub func(string) (cargo.Config, error) + } +} + +func (f *ConfigParser) Parse(param1 string) (cargo.Config, error) { + f.ParseCall.Lock() + defer f.ParseCall.Unlock() + f.ParseCall.CallCount++ + f.ParseCall.Receives.Path = param1 + if f.ParseCall.Stub != nil { + return f.ParseCall.Stub(param1) + } + return f.ParseCall.Returns.Config, f.ParseCall.Returns.Error +} diff --git a/cargo/jam/commands/fakes/file_bundler.go b/cargo/jam/commands/fakes/file_bundler.go new file mode 100644 index 00000000..f482e98d --- /dev/null +++ b/cargo/jam/commands/fakes/file_bundler.go @@ -0,0 +1,37 @@ +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/packit/cargo" +) + +type FileBundler struct { + BundleCall struct { + sync.Mutex + CallCount int + Receives struct { + Path string + Files []string + Config cargo.Config + } + Returns struct { + FileSlice []cargo.File + Error error + } + Stub func(string, []string, cargo.Config) ([]cargo.File, error) + } +} + +func (f *FileBundler) Bundle(param1 string, param2 []string, param3 cargo.Config) ([]cargo.File, error) { + f.BundleCall.Lock() + defer f.BundleCall.Unlock() + f.BundleCall.CallCount++ + f.BundleCall.Receives.Path = param1 + f.BundleCall.Receives.Files = param2 + f.BundleCall.Receives.Config = param3 + if f.BundleCall.Stub != nil { + return f.BundleCall.Stub(param1, param2, param3) + } + return f.BundleCall.Returns.FileSlice, f.BundleCall.Returns.Error +} diff --git a/cargo/jam/commands/fakes/prepackager.go b/cargo/jam/commands/fakes/prepackager.go new file mode 100644 index 00000000..3f61a2d4 --- /dev/null +++ b/cargo/jam/commands/fakes/prepackager.go @@ -0,0 +1,30 @@ +package fakes + +import "sync" + +type PrePackager struct { + ExecuteCall struct { + sync.Mutex + CallCount int + Receives struct { + Path string + RootDir string + } + Returns struct { + Error error + } + Stub func(string, string) error + } +} + +func (f *PrePackager) Execute(param1 string, param2 string) error { + f.ExecuteCall.Lock() + defer f.ExecuteCall.Unlock() + f.ExecuteCall.CallCount++ + f.ExecuteCall.Receives.Path = param1 + f.ExecuteCall.Receives.RootDir = param2 + if f.ExecuteCall.Stub != nil { + return f.ExecuteCall.Stub(param1, param2) + } + return f.ExecuteCall.Returns.Error +} diff --git a/cargo/jam/commands/fakes/tar_builder.go b/cargo/jam/commands/fakes/tar_builder.go new file mode 100644 index 00000000..95b2c4dd --- /dev/null +++ b/cargo/jam/commands/fakes/tar_builder.go @@ -0,0 +1,34 @@ +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/packit/cargo" +) + +type TarBuilder struct { + BuildCall struct { + sync.Mutex + CallCount int + Receives struct { + Path string + Files []cargo.File + } + Returns struct { + Error error + } + Stub func(string, []cargo.File) error + } +} + +func (f *TarBuilder) Build(param1 string, param2 []cargo.File) error { + f.BuildCall.Lock() + defer f.BuildCall.Unlock() + f.BuildCall.CallCount++ + f.BuildCall.Receives.Path = param1 + f.BuildCall.Receives.Files = param2 + if f.BuildCall.Stub != nil { + return f.BuildCall.Stub(param1, param2) + } + return f.BuildCall.Returns.Error +} diff --git a/cargo/jam/commands/init_test.go b/cargo/jam/commands/init_test.go new file mode 100644 index 00000000..a1ae7fc1 --- /dev/null +++ b/cargo/jam/commands/init_test.go @@ -0,0 +1,52 @@ +package commands_test + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitCommands(t *testing.T) { + suite := spec.New("jam/commands", spec.Report(report.Terminal{})) + suite("Pack", testPack) + suite.Run(t) +} + +func ExtractFile(file *os.File, name string) ([]byte, error) { + _, err := file.Seek(0, 0) + if err != nil { + return nil, err + } + + //TODO: Replace me with decompression library + gzr, err := gzip.NewReader(file) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + hdr, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + if hdr.Name == name { + return ioutil.ReadAll(tr) + } + } + + return nil, fmt.Errorf("no such file: %s", name) +} diff --git a/cargo/jam/commands/pack.go b/cargo/jam/commands/pack.go new file mode 100644 index 00000000..b3207ae0 --- /dev/null +++ b/cargo/jam/commands/pack.go @@ -0,0 +1,104 @@ +package commands + +import ( + "errors" + "flag" + "fmt" + "io" + "path/filepath" + + "github.com/cloudfoundry/packit/cargo" +) + +//go:generate faux --interface ConfigParser --output fakes/config_parser.go +type ConfigParser interface { + Parse(path string) (cargo.Config, error) +} + +//go:generate faux --interface FileBundler --output fakes/file_bundler.go +type FileBundler interface { + Bundle(path string, files []string, config cargo.Config) ([]cargo.File, error) +} + +//go:generate faux --interface TarBuilder --output fakes/tar_builder.go +type TarBuilder interface { + Build(path string, files []cargo.File) error +} + +//go:generate faux --interface PrePackager --output fakes/prepackager.go +type PrePackager interface { + Execute(path, rootDir string) error +} + +type Pack struct { + configParser ConfigParser + tarBuilder TarBuilder + fileBundler FileBundler + prePackager PrePackager + stdout io.Writer +} + +func NewPack(configParser ConfigParser, prePackager PrePackager, fileBundler FileBundler, tarBuilder TarBuilder, stdout io.Writer) Pack { + return Pack{ + configParser: configParser, + tarBuilder: tarBuilder, + fileBundler: fileBundler, + prePackager: prePackager, + stdout: stdout, + } +} + +func (p Pack) Execute(args []string) error { + var ( + buildpackTOMLPath string + output string + version string + ) + + fset := flag.NewFlagSet("pack", flag.ContinueOnError) + fset.StringVar(&buildpackTOMLPath, "buildpack", "", "path to buildpack.toml") + fset.StringVar(&output, "output", "", "path to location of output tarball") + fset.StringVar(&version, "version", "", "version of the buildpack") + err := fset.Parse(args) + if err != nil { + return err + } + + if buildpackTOMLPath == "" { + return errors.New("missing required flag --buildpack") + } + + if output == "" { + return errors.New("missing required flag --output") + } + + if version == "" { + return errors.New("missing required flag --version") + } + + config, err := p.configParser.Parse(buildpackTOMLPath) + if err != nil { + return fmt.Errorf("failed to parse buildpack.toml: %s", err) + } + + config.Buildpack.Version = version + + err = p.prePackager.Execute(config.Metadata.PrePackage, filepath.Dir(buildpackTOMLPath)) + if err != nil { + return fmt.Errorf("failed to execute pre-packaging script %q: %s", config.Metadata.PrePackage, err) + } + + fmt.Fprintf(p.stdout, "Packing %s %s...\n", config.Buildpack.Name, version) + + files, err := p.fileBundler.Bundle(filepath.Dir(buildpackTOMLPath), config.Metadata.IncludeFiles, config) + if err != nil { + return fmt.Errorf("failed to bundle files: %s", err) + } + + err = p.tarBuilder.Build(output, files) + if err != nil { + return fmt.Errorf("failed to create output: %s", err) + } + + return nil +} diff --git a/cargo/jam/commands/pack_test.go b/cargo/jam/commands/pack_test.go new file mode 100644 index 00000000..eb76f454 --- /dev/null +++ b/cargo/jam/commands/pack_test.go @@ -0,0 +1,206 @@ +package commands_test + +import ( + "bytes" + "errors" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cloudfoundry/packit/cargo" + "github.com/cloudfoundry/packit/cargo/jam/commands" + "github.com/cloudfoundry/packit/cargo/jam/commands/fakes" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testPack(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + buffer *bytes.Buffer + configParser *fakes.ConfigParser + tarBuilder *fakes.TarBuilder + prePackager *fakes.PrePackager + fileBundler *fakes.FileBundler + tempDir string + + command commands.Pack + ) + + it.Before(func() { + configParser = &fakes.ConfigParser{} + configParser.ParseCall.Returns.Config = cargo.Config{ + API: "0.2", + Buildpack: cargo.ConfigBuildpack{ + ID: "some-buildpack-id", + Name: "some-buildpack-name", + }, + Metadata: cargo.ConfigMetadata{ + IncludeFiles: []string{ + "bin/build", + "bin/detect", + "buildpack.toml", + }, + PrePackage: "some-prepackage-script", + }, + } + + fileBundler = &fakes.FileBundler{} + fileBundler.BundleCall.Returns.FileSlice = []cargo.File{ + { + Name: "buildpack.toml", + Size: int64(len("buildpack-toml-contents")), + ReadCloser: ioutil.NopCloser(strings.NewReader("buildpack-toml-contents")), + }, + } + + tarBuilder = &fakes.TarBuilder{} + + buffer = bytes.NewBuffer(nil) + + prePackager = &fakes.PrePackager{} + + command = commands.NewPack(configParser, prePackager, fileBundler, tarBuilder, buffer) + + var err error + tempDir, err = ioutil.TempDir("", "buildpack") + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + }) + + context("Execute", func() { + it("builds a buildpack", func() { + err := command.Execute([]string{ + "--buildpack", "buildpack-root/some-buildpack.toml", + "--version", "some-buildpack-version", + "--output", "some-output.tgz", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer).To(ContainSubstring("Packing some-buildpack-name some-buildpack-version...\n")) + + Expect(prePackager.ExecuteCall.Receives.Path).To(Equal("some-prepackage-script")) + Expect(prePackager.ExecuteCall.Receives.RootDir).To(Equal("buildpack-root")) + + Expect(configParser.ParseCall.Receives.Path).To(Equal("buildpack-root/some-buildpack.toml")) + + Expect(tarBuilder.BuildCall.Receives.Path).To(Equal("some-output.tgz")) + Expect(tarBuilder.BuildCall.Receives.Files).To(HaveLen(1)) + + buildpackTOMLFile := tarBuilder.BuildCall.Receives.Files[0] + Expect(buildpackTOMLFile.Name).To(Equal("buildpack.toml")) + Expect(buildpackTOMLFile.Size).To(Equal(int64(len("buildpack-toml-contents")))) + + contents, err := ioutil.ReadAll(buildpackTOMLFile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(contents)).To(Equal("buildpack-toml-contents")) + }) + }) + + context("failure cases", func() { + context("when given an unknown flag", func() { + it("prints an error message", func() { + err := command.Execute([]string{"--unknown"}) + Expect(err).To(MatchError(ContainSubstring("flag provided but not defined: -unknown"))) + }) + }) + context("when the --buildpack flag is empty", func() { + it("prints an error message", func() { + err := command.Execute([]string{ + "--version", "some-buildpack-version", + "--output", filepath.Join(tempDir, "tempBuildpack.tar.gz"), + }) + Expect(err).To(MatchError("missing required flag --buildpack")) + }) + }) + + context("when the --output flag is empty", func() { + it("prints an error message", func() { + err := command.Execute([]string{ + "--buildpack", filepath.Join("..", "testdata", "example-cnb", "buildpack.toml"), + "--version", "some-buildpack-version", + }) + Expect(err).To(MatchError("missing required flag --output")) + }) + }) + + context("when the --version flag is empty", func() { + it("prints an error message", func() { + err := command.Execute([]string{ + "--buildpack", filepath.Join("..", "testdata", "example-cnb", "buildpack.toml"), + "--output", filepath.Join(tempDir, "tempBuildpack.tar.gz"), + }) + Expect(err).To(MatchError("missing required flag --version")) + }) + }) + + context("when the buildpack parser fails", func() { + it.Before(func() { + configParser.ParseCall.Returns.Error = errors.New("failed to parse") + }) + + it("returns an error", func() { + err := command.Execute([]string{ + "--buildpack", "no-such-buildpack.toml", + "--output", filepath.Join(tempDir, "tempBuildpack.tar.gz"), + "--version", "some-buildpack-version", + }) + Expect(err).To(MatchError(ContainSubstring("failed to parse buildpack.toml:"))) + Expect(err).To(MatchError(ContainSubstring("failed to parse"))) + }) + }) + + context("when the prepackager fails", func() { + it.Before(func() { + prePackager.ExecuteCall.Returns.Error = errors.New("script failed") + }) + + it("returns an error", func() { + err := command.Execute([]string{ + "--buildpack", "no-such-buildpack.toml", + "--output", filepath.Join(tempDir, "tempBuildpack.tar.gz"), + "--version", "some-buildpack-version", + }) + Expect(err).To(MatchError("failed to execute pre-packaging script \"some-prepackage-script\": script failed")) + }) + }) + + context("when the files cannot be bundled", func() { + it.Before(func() { + fileBundler.BundleCall.Returns.Error = errors.New("read failed") + }) + + it("returns an error", func() { + err := command.Execute([]string{ + "--buildpack", "no-such-buildpack.toml", + "--output", "output.tar.gz", + "--version", "some-buildpack-version", + }) + Expect(err).To(MatchError(ContainSubstring("failed to bundle files:"))) + Expect(err).To(MatchError(ContainSubstring("read failed"))) + }) + }) + + context("when the tar builder failes", func() { + it.Before(func() { + tarBuilder.BuildCall.Returns.Error = errors.New("failed to build tarball") + }) + + it("returns an error", func() { + err := command.Execute([]string{ + "--buildpack", "some-buildpack.toml", + "--output", "some-output.tgz", + "--version", "some-buildpack-version", + }) + Expect(err).To(MatchError(ContainSubstring("failed to create output:"))) + Expect(err).To(MatchError(ContainSubstring("failed to build tarball"))) + }) + }) + }) +} diff --git a/cargo/jam/errors_test.go b/cargo/jam/errors_test.go new file mode 100644 index 00000000..cba68ec4 --- /dev/null +++ b/cargo/jam/errors_test.go @@ -0,0 +1,51 @@ +package main_test + +import ( + "bytes" + "os/exec" + "testing" + + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testErrors(t *testing.T, context spec.G, it spec.S) { + var ( + withT = NewWithT(t) + Expect = withT.Expect + Eventually = withT.Eventually + + buffer *bytes.Buffer + ) + + it.Before(func() { + buffer = bytes.NewBuffer(nil) + }) + + context("failure cases", func() { + context("when there is no command", func() { + it("prints an error message", func() { + command := exec.Command(path) + session, err := gexec.Start(command, buffer, buffer) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(1), func() string { return buffer.String() }) + + Expect(session.Err).To(gbytes.Say("missing command")) + }) + }) + + context("when the given command is unknown", func() { + it("prints an error message", func() { + command := exec.Command(path, "some-unknown-command") + session, err := gexec.Start(command, buffer, buffer) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(1), func() string { return buffer.String() }) + + Expect(session.Err).To(gbytes.Say("unknown command: \"some-unknown-command\"")) + }) + }) + }) +} diff --git a/cargo/jam/init_test.go b/cargo/jam/init_test.go new file mode 100644 index 00000000..8563ce95 --- /dev/null +++ b/cargo/jam/init_test.go @@ -0,0 +1,79 @@ +package main_test + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/onsi/gomega/gexec" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + . "github.com/onsi/gomega" +) + +var path string + +func TestUnitJam(t *testing.T) { + suite := spec.New("cargo/jam", spec.Report(report.Terminal{})) + suite("pack", testPack) + suite("Errors", testErrors) + + suite.Before(func(t *testing.T) { + var ( + Expect = NewWithT(t).Expect + err error + ) + + path, err = gexec.Build("github.com/cloudfoundry/packit/cargo/jam") + Expect(err).NotTo(HaveOccurred()) + }) + + suite.After(func(t *testing.T) { + gexec.CleanupBuildArtifacts() + }) + + suite.Run(t) +} + +func ExtractFile(file *os.File, name string) ([]byte, *tar.Header, error) { + _, err := file.Seek(0, 0) + if err != nil { + return nil, nil, err + } + + //TODO: Replace me with decompression library + gzr, err := gzip.NewReader(file) + if err != nil { + return nil, nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + hdr, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, nil, err + } + + if hdr.Name == name { + contents, err := ioutil.ReadAll(tr) + if err != nil { + return nil, nil, err + } + + return contents, hdr, nil + } + + } + + return nil, nil, fmt.Errorf("no such file: %s", name) +} diff --git a/cargo/jam/main.go b/cargo/jam/main.go new file mode 100644 index 00000000..dddefded --- /dev/null +++ b/cargo/jam/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + + "code.cloudfoundry.org/lager" + + "github.com/cloudfoundry/packit/cargo" + "github.com/cloudfoundry/packit/cargo/jam/commands" + "github.com/cloudfoundry/packit/pexec" +) + +func main() { + if len(os.Args) < 2 { + fail("missing command") + } + + switch os.Args[1] { + case "pack": + buildpackParser := cargo.NewBuildpackParser() + fileBundler := cargo.NewFileBundler() + tarBuilder := cargo.NewTarBuilder() + prePackager := cargo.NewPrePackager(pexec.NewExecutable("bash", lager.NewLogger("pre-packager"))) + command := commands.NewPack(buildpackParser, prePackager, fileBundler, tarBuilder, os.Stdout) + + if err := command.Execute(os.Args[2:]); err != nil { + fail("failed to execute pack command: %s", err) + } + + default: + fail("unknown command: %q", os.Args[1]) + } +} + +func fail(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, format, v...) + os.Exit(1) +} diff --git a/cargo/jam/pack_test.go b/cargo/jam/pack_test.go new file mode 100644 index 00000000..b6b4831e --- /dev/null +++ b/cargo/jam/pack_test.go @@ -0,0 +1,100 @@ +package main_test + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/sclevine/spec" + + . "github.com/cloudfoundry/packit/matchers" + . "github.com/onsi/gomega" +) + +func testPack(t *testing.T, context spec.G, it spec.S) { + var ( + withT = NewWithT(t) + Expect = withT.Expect + Eventually = withT.Eventually + + buffer *bytes.Buffer + tmpDir string + ) + + it.Before(func() { + buffer = bytes.NewBuffer(nil) + + var err error + tmpDir, err = ioutil.TempDir("", "output") + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + Expect(os.RemoveAll(filepath.Join("testdata", "example-cnb", "generated-file"))).To(Succeed()) + }) + + it("creates a packaged buildpack", func() { + command := exec.Command( + path, "pack", + "--buildpack", filepath.Join("testdata", "example-cnb", "buildpack.toml"), + "--output", filepath.Join(tmpDir, "output.tgz"), + "--version", "some-version", + ) + session, err := gexec.Start(command, buffer, buffer) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0), func() string { return buffer.String() }) + + Expect(session.Out).To(gbytes.Say("Packing some-buildpack-name some-version...")) + + file, err := os.Open(filepath.Join(tmpDir, "output.tgz")) + Expect(err).ToNot(HaveOccurred()) + + contents, hdr, err := ExtractFile(file, "buildpack.toml") + Expect(err).ToNot(HaveOccurred()) + Expect(contents).To(MatchTOML(`api = "0.2" + +[buildpack] +id = "some-buildpack-id" +name = "some-buildpack-name" +version = "some-version" + +[metadata] +include_files = ["bin/build", "bin/detect", "buildpack.toml", "generated-file"] +pre_package = "./scripts/build.sh"`)) + Expect(hdr.Mode).To(Equal(int64(0644))) + + contents, hdr, err = ExtractFile(file, "bin/build") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("build-contents")) + Expect(hdr.Mode).To(Equal(int64(0755))) + + contents, hdr, err = ExtractFile(file, "bin/detect") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("detect-contents")) + Expect(hdr.Mode).To(Equal(int64(0755))) + + contents, hdr, err = ExtractFile(file, "generated-file") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("hello\n")) + Expect(hdr.Mode).To(Equal(int64(0644))) + }) + + context("failure cases", func() { + context("when the --buildpack flag is empty", func() { + it("prints an error message", func() { + command := exec.Command(path, "pack") + session, err := gexec.Start(command, buffer, buffer) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(1), func() string { return buffer.String() }) + + Expect(session.Err).To(gbytes.Say("missing required flag --buildpack")) + }) + }) + }) +} diff --git a/cargo/jam/testdata/example-cnb/bin/build b/cargo/jam/testdata/example-cnb/bin/build new file mode 100755 index 00000000..c76df1a2 --- /dev/null +++ b/cargo/jam/testdata/example-cnb/bin/build @@ -0,0 +1 @@ +build-contents \ No newline at end of file diff --git a/cargo/jam/testdata/example-cnb/bin/detect b/cargo/jam/testdata/example-cnb/bin/detect new file mode 100755 index 00000000..f2091a73 --- /dev/null +++ b/cargo/jam/testdata/example-cnb/bin/detect @@ -0,0 +1 @@ +detect-contents \ No newline at end of file diff --git a/cargo/jam/testdata/example-cnb/buildpack.toml b/cargo/jam/testdata/example-cnb/buildpack.toml new file mode 100644 index 00000000..abefd28e --- /dev/null +++ b/cargo/jam/testdata/example-cnb/buildpack.toml @@ -0,0 +1,10 @@ +api = "0.2" + +[buildpack] + id = "some-buildpack-id" + name = "some-buildpack-name" + version = "version-string" + +[metadata] +include_files = ["bin/build", "bin/detect", "buildpack.toml", "generated-file"] +pre_package = "./scripts/build.sh" diff --git a/cargo/jam/testdata/example-cnb/scripts/build.sh b/cargo/jam/testdata/example-cnb/scripts/build.sh new file mode 100755 index 00000000..f8b5579e --- /dev/null +++ b/cargo/jam/testdata/example-cnb/scripts/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +readonly PROGDIR="$(cd "$(dirname "${0}")" && pwd)" + +echo "hello" > "$PROGDIR/../generated-file" diff --git a/cargo/pre_packager.go b/cargo/pre_packager.go new file mode 100644 index 00000000..21de246c --- /dev/null +++ b/cargo/pre_packager.go @@ -0,0 +1,31 @@ +package cargo + +import ( + "github.com/cloudfoundry/packit/pexec" +) + +//go:generate faux --interface Executable --output fakes/executable.go +type Executable interface { + Execute(execution pexec.Execution) (stdOut string, stdError string, err error) +} + +type PrePackager struct { + executable Executable +} + +func NewPrePackager(executable Executable) PrePackager { + return PrePackager{ + executable: executable, + } +} + +func (p PrePackager) Execute(scriptPath, rootDir string) error { + if scriptPath == "" { + return nil + } + _, _, err := p.executable.Execute(pexec.Execution{ + Args: []string{"-c", scriptPath}, + Dir: rootDir, + }) + return err +} diff --git a/cargo/pre_packager_test.go b/cargo/pre_packager_test.go new file mode 100644 index 00000000..3c808e4a --- /dev/null +++ b/cargo/pre_packager_test.go @@ -0,0 +1,44 @@ +package cargo_test + +import ( + "testing" + + "github.com/cloudfoundry/packit/cargo" + "github.com/cloudfoundry/packit/cargo/fakes" + "github.com/cloudfoundry/packit/pexec" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testPrePackager(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + bash *fakes.Executable + prePackager cargo.PrePackager + ) + + it.Before(func() { + bash = &fakes.Executable{} + + prePackager = cargo.NewPrePackager(bash) + }) + + context("Execute", func() { + it("executes the pre_package script", func() { + err := prePackager.Execute("some-script", "some-dir") + Expect(err).NotTo(HaveOccurred()) + Expect(bash.ExecuteCall.Receives.Execution).To(Equal(pexec.Execution{ + Args: []string{"-c", "some-script"}, + Dir: "some-dir", + })) + }) + + it("executes nothing when passed a empty script", func() { + err := prePackager.Execute("", "some-dir") + Expect(err).NotTo(HaveOccurred()) + Expect(bash.ExecuteCall.CallCount).To(Equal(0)) + }) + }) +} diff --git a/cargo/tar_builder.go b/cargo/tar_builder.go new file mode 100644 index 00000000..7e4d2bbe --- /dev/null +++ b/cargo/tar_builder.go @@ -0,0 +1,57 @@ +package cargo + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" +) + +type File struct { + io.ReadCloser + + Name string + Size int64 + Mode int64 +} + +type TarBuilder struct{} + +func NewTarBuilder() TarBuilder { + return TarBuilder{} +} + +func (b TarBuilder) Build(path string, files []File) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to create tarball: %s", err) + } + defer file.Close() + + gw := gzip.NewWriter(file) + defer gw.Close() + + tw := tar.NewWriter(gw) + defer tw.Close() + + for _, file := range files { + err = tw.WriteHeader(&tar.Header{ + Name: file.Name, + Size: file.Size, + Mode: file.Mode, + }) + if err != nil { + return fmt.Errorf("failed to write header to tarball: %s", err) + } + + _, err = io.Copy(tw, file) + if err != nil { + return fmt.Errorf("failed to write file to tarball: %s", err) + } + + file.Close() + } + + return nil +} diff --git a/cargo/tar_builder_test.go b/cargo/tar_builder_test.go new file mode 100644 index 00000000..b5a38856 --- /dev/null +++ b/cargo/tar_builder_test.go @@ -0,0 +1,121 @@ +package cargo_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cloudfoundry/packit/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testTarBuilder(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + tempFile string + tempDir string + builder cargo.TarBuilder + ) + + it.Before(func() { + var err error + tempDir, err = ioutil.TempDir("", "output") + Expect(err).ToNot(HaveOccurred()) + + tempFile = filepath.Join(tempDir, "buildpack.tgz") + + builder = cargo.NewTarBuilder() + }) + + it.After(func() { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + }) + + context("Build", func() { + context("given a destination and a list of files", func() { + it("constructs a tar ball", func() { + err := builder.Build(tempFile, []cargo.File{ + { + Name: "bin/build", + Size: int64(len("build-contents")), + Mode: int64(0755), + ReadCloser: ioutil.NopCloser(strings.NewReader("build-contents")), + }, + { + Name: "bin/detect", + Size: int64(len("detect-contents")), + Mode: int64(0755), + ReadCloser: ioutil.NopCloser(strings.NewReader("detect-contents")), + }, + { + Name: "buildpack.toml", + Size: int64(len("buildpack-toml-contents")), + Mode: int64(0644), + ReadCloser: ioutil.NopCloser(strings.NewReader("buildpack-toml-contents")), + }, + }) + Expect(err).ToNot(HaveOccurred()) + + file, err := os.Open(tempFile) + Expect(err).ToNot(HaveOccurred()) + + contents, hdr, err := ExtractFile(file, "buildpack.toml") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("buildpack-toml-contents")) + Expect(hdr.Mode).To(Equal(int64(0644))) + + contents, hdr, err = ExtractFile(file, "bin/build") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("build-contents")) + Expect(hdr.Mode).To(Equal(int64(0755))) + + contents, hdr, err = ExtractFile(file, "bin/detect") + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("detect-contents")) + Expect(hdr.Mode).To(Equal(int64(0755))) + }) + }) + + context("failure cases", func() { + context("when it is unable to create the destination file", func() { + it.Before(func() { + Expect(os.Chmod(tempDir, 0000)).To(Succeed()) + }) + + it.Before(func() { + Expect(os.Chmod(tempDir, 0644)).To(Succeed()) + }) + + it("returns an error", func() { + err := builder.Build(tempFile, []cargo.File{ + { + Name: "bin/build", + Size: int64(len("build-contents")), + ReadCloser: ioutil.NopCloser(strings.NewReader("build-contents")), + }, + }) + Expect(err).To(MatchError(ContainSubstring("failed to create tarball"))) + Expect(err).To(MatchError(ContainSubstring("permission denied"))) + }) + }) + + context("when one of the files cannot be written", func() { + it("returns an error", func() { + err := builder.Build(tempFile, []cargo.File{ + { + Name: "bin/build", + ReadCloser: ioutil.NopCloser(strings.NewReader("build-contents")), + }, + }) + Expect(err).To(MatchError(ContainSubstring("failed to write file to tarball"))) + Expect(err).To(MatchError(ContainSubstring("write too long"))) + }) + }) + }) + }) +} diff --git a/detect_test.go b/detect_test.go index 45f9f462..9a97d331 100644 --- a/detect_test.go +++ b/detect_test.go @@ -11,6 +11,7 @@ import ( "github.com/cloudfoundry/packit/fakes" "github.com/sclevine/spec" + . "github.com/cloudfoundry/packit/matchers" . "github.com/onsi/gomega" ) diff --git a/go.mod b/go.mod index c93b806e..3c717d6f 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,24 @@ require ( code.cloudfoundry.org/lager v2.0.0+incompatible github.com/BurntSushi/toml v0.3.1 github.com/cheggaaa/pb v2.0.7+incompatible + github.com/golang/protobuf v1.3.2 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect - github.com/onsi/ginkgo v1.10.2 - github.com/onsi/gomega v1.7.0 + github.com/onsi/ginkgo v1.10.3 // indirect + github.com/onsi/gomega v1.7.2-0.20191118193045-1526230ca7de + github.com/pelletier/go-toml v1.6.0 github.com/sclevine/spec v1.3.0 + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/sys v0.0.0-20191119195528-f068ffe820e4 // indirect + golang.org/x/text v0.3.2 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect gopkg.in/VividCortex/ewma.v1 v1.1.1 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/cheggaaa/pb.v2 v2.0.7 // indirect gopkg.in/fatih/color.v1 v1.7.0 // indirect gopkg.in/mattn/go-colorable.v0 v0.1.0 // indirect gopkg.in/mattn/go-isatty.v0 v0.0.4 // indirect gopkg.in/mattn/go-runewidth.v0 v0.0.4 // indirect + gopkg.in/yaml.v2 v2.2.7 // indirect ) diff --git a/go.sum b/go.sum index 50b4dc22..c71c38a3 100644 --- a/go.sum +++ b/go.sum @@ -4,40 +4,65 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cheggaaa/pb v2.0.7+incompatible h1:gLKifR1UkZ/kLkda5gC0K6c8g+jU2sINPtBeOiNlMhU= github.com/cheggaaa/pb v2.0.7+incompatible/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/cloudfoundry/node-engine-cnb v0.0.109 h1:fFgEfl8nayo4C7GnunSCT7jXm8iPkoPchizoshW77ko= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.2-0.20191118193045-1526230ca7de h1:z00YOqD1uWdJeFSTZin+QLAJtaLId7KfP+CZIYhilMs= +github.com/onsi/gomega v1.7.2-0.20191118193045-1526230ca7de/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/sclevine/spec v1.3.0 h1:iTB51CYlnju5oRh0/l67fg1+RlQ2nqmFecwdvN+5TrI= github.com/sclevine/spec v1.3.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191119195528-f068ffe820e4 h1:FjhQftcbpdYXneEYSWZO7+6Bu+Bi1A8VPvGYWOIzIbw= +golang.org/x/sys v0.0.0-20191119195528-f068ffe820e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/VividCortex/ewma.v1 v1.1.1 h1:tWHEKkKq802K/JT9RiqGCBU5fW3raAPnJGTE9ostZvg= gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v2 v2.0.7 h1:beaAg8eacCdMQS9Y7obFEtkY7gQl0uZ6Zayb3ry41VY= gopkg.in/cheggaaa/pb.v2 v2.0.7/go.mod h1:0CiZ1p8pvtxBlQpLXkHuUTpdJ1shm3OqCF1QugkjHL4= gopkg.in/fatih/color.v1 v1.7.0 h1:bYGjb+HezBM6j/QmgBfgm1adxHpzzrss6bj4r9ROppk= @@ -52,5 +77,7 @@ gopkg.in/mattn/go-runewidth.v0 v0.0.4 h1:r0P71TnzQDlNIcizCqvPSSANoFa3WVGtcNJf3TW gopkg.in/mattn/go-runewidth.v0 v0.0.4/go.mod h1:BmXejnxvhwdaATwiJbB1vZ2dtXkQKZGu9yLFCZb4msQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/init_test.go b/init_test.go index a2446a6f..e9f6e6d3 100644 --- a/init_test.go +++ b/init_test.go @@ -1,12 +1,8 @@ package packit_test import ( - "fmt" - "reflect" "testing" - "github.com/BurntSushi/toml" - "github.com/onsi/gomega/types" "github.com/sclevine/spec" "github.com/sclevine/spec/report" ) @@ -19,57 +15,3 @@ func TestUnitPackit(t *testing.T) { suite("Layers", testLayers) suite.Run(t) } - -func MatchTOML(expected interface{}) types.GomegaMatcher { - return &matchTOML{ - expected: expected, - } -} - -type matchTOML struct { - expected interface{} -} - -func (matcher *matchTOML) Match(actual interface{}) (success bool, err error) { - var e, a string - - switch eType := matcher.expected.(type) { - case string: - e = eType - case []byte: - e = string(eType) - default: - return false, fmt.Errorf("expected value must be []byte or string, received %T", matcher.expected) - } - - switch aType := actual.(type) { - case string: - a = aType - case []byte: - a = string(aType) - default: - return false, fmt.Errorf("actual value must be []byte or string, received %T", matcher.expected) - } - - var eValue map[string]interface{} - _, err = toml.Decode(e, &eValue) - if err != nil { - return false, err - } - - var aValue map[string]interface{} - _, err = toml.Decode(a, &aValue) - if err != nil { - return false, err - } - - return reflect.DeepEqual(eValue, aValue), nil -} - -func (matcher *matchTOML) FailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n%s\nto match the TOML representation of\n%s", actual, matcher.expected) -} - -func (matcher *matchTOML) NegatedFailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n%s\nnot to match the TOML representation of\n%s", actual, matcher.expected) -} diff --git a/internal/init_test.go b/internal/init_test.go index 99dbc74f..e30c9e54 100644 --- a/internal/init_test.go +++ b/internal/init_test.go @@ -1,12 +1,8 @@ package internal_test import ( - "fmt" - "reflect" "testing" - "github.com/BurntSushi/toml" - "github.com/onsi/gomega/types" "github.com/sclevine/spec" "github.com/sclevine/spec/report" ) @@ -18,57 +14,3 @@ func TestUnitInternal(t *testing.T) { suite("TOMLWriter", testTOMLWriter) suite.Run(t) } - -func MatchTOML(expected interface{}) types.GomegaMatcher { - return &matchTOML{ - expected: expected, - } -} - -type matchTOML struct { - expected interface{} -} - -func (matcher *matchTOML) Match(actual interface{}) (success bool, err error) { - var e, a string - - switch eType := matcher.expected.(type) { - case string: - e = eType - case []byte: - e = string(eType) - default: - return false, fmt.Errorf("expected value must be []byte or string, received %T", matcher.expected) - } - - switch aType := actual.(type) { - case string: - a = aType - case []byte: - a = string(aType) - default: - return false, fmt.Errorf("actual value must be []byte or string, received %T", matcher.expected) - } - - var eValue map[string]interface{} - _, err = toml.Decode(e, &eValue) - if err != nil { - return false, err - } - - var aValue map[string]interface{} - _, err = toml.Decode(a, &aValue) - if err != nil { - return false, err - } - - return reflect.DeepEqual(eValue, aValue), nil -} - -func (matcher *matchTOML) FailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n%s\nto match the TOML representation of\n%s", actual, matcher.expected) -} - -func (matcher *matchTOML) NegatedFailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n%s\nnot to match the TOML representation of\n%s", actual, matcher.expected) -} diff --git a/internal/toml_writer_test.go b/internal/toml_writer_test.go index 1fbbefe0..c062fc63 100644 --- a/internal/toml_writer_test.go +++ b/internal/toml_writer_test.go @@ -7,9 +7,10 @@ import ( "testing" "github.com/cloudfoundry/packit/internal" + "github.com/sclevine/spec" + . "github.com/cloudfoundry/packit/matchers" . "github.com/onsi/gomega" - "github.com/sclevine/spec" ) func testTOMLWriter(t *testing.T, context spec.G, it spec.S) { diff --git a/matchers/match_toml.go b/matchers/match_toml.go new file mode 100644 index 00000000..1922fe02 --- /dev/null +++ b/matchers/match_toml.go @@ -0,0 +1,63 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/BurntSushi/toml" + "github.com/onsi/gomega/types" +) + +func MatchTOML(expected interface{}) types.GomegaMatcher { + return &matchTOML{ + expected: expected, + } +} + +type matchTOML struct { + expected interface{} +} + +func (matcher *matchTOML) Match(actual interface{}) (success bool, err error) { + var e, a string + + switch eType := matcher.expected.(type) { + case string: + e = eType + case []byte: + e = string(eType) + default: + return false, fmt.Errorf("expected value must be []byte or string, received %T", matcher.expected) + } + + switch aType := actual.(type) { + case string: + a = aType + case []byte: + a = string(aType) + default: + return false, fmt.Errorf("actual value must be []byte or string, received %T", matcher.expected) + } + + var eValue map[string]interface{} + _, err = toml.Decode(e, &eValue) + if err != nil { + return false, err + } + + var aValue map[string]interface{} + _, err = toml.Decode(a, &aValue) + if err != nil { + return false, err + } + + return reflect.DeepEqual(eValue, aValue), nil +} + +func (matcher *matchTOML) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto match the TOML representation of\n%s", actual, matcher.expected) +} + +func (matcher *matchTOML) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to match the TOML representation of\n%s", actual, matcher.expected) +}