From b45383fd438b8c88cee35720ce7d56dbf3c3d054 Mon Sep 17 00:00:00 2001 From: Antoine Cotten Date: Mon, 24 Apr 2023 15:49:49 +0200 Subject: [PATCH] test(e2e): add CLI test for pkg update Signed-off-by: Antoine Cotten --- .github/workflows/e2e-cli.yaml | 3 + test/e2e/cli/pkg_test.go | 83 +++++++++++ test/e2e/cli/version_test.go | 1 + test/e2e/framework/matchers/be_empty_dir.go | 51 +++++++ .../framework/matchers/be_empty_dir_test.go | 66 +++++++++ test/e2e/framework/matchers/contain_dirs.go | 65 ++++++++ .../framework/matchers/contain_dirs_test.go | 63 ++++++++ test/e2e/framework/matchers/contain_files.go | 65 ++++++++ .../framework/matchers/contain_files_test.go | 140 ++++++++++++++++++ test/e2e/framework/matchers/matchers.go | 33 +++++ 10 files changed, 570 insertions(+) create mode 100644 test/e2e/cli/pkg_test.go create mode 100644 test/e2e/framework/matchers/be_empty_dir.go create mode 100644 test/e2e/framework/matchers/be_empty_dir_test.go create mode 100644 test/e2e/framework/matchers/contain_dirs.go create mode 100644 test/e2e/framework/matchers/contain_dirs_test.go create mode 100644 test/e2e/framework/matchers/contain_files.go create mode 100644 test/e2e/framework/matchers/contain_files_test.go create mode 100644 test/e2e/framework/matchers/matchers.go diff --git a/.github/workflows/e2e-cli.yaml b/.github/workflows/e2e-cli.yaml index 7ac2d57ea..6160c7219 100644 --- a/.github/workflows/e2e-cli.yaml +++ b/.github/workflows/e2e-cli.yaml @@ -46,6 +46,9 @@ jobs: - name: Install kraft run: make kraft DOCKER= DISTDIR="$(go env GOPATH)"/bin + - name: Run unit tests + run: ginkgo -v -p -randomize-all ./test/e2e/framework/... + - name: Run e2e tests env: KRAFTKIT_NO_CHECK_UPDATES: true diff --git a/test/e2e/cli/pkg_test.go b/test/e2e/cli/pkg_test.go new file mode 100644 index 000000000..34ac0c75d --- /dev/null +++ b/test/e2e/cli/pkg_test.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package cli_test + +import ( + . "github.com/onsi/ginkgo/v2" //nolint:stylecheck + . "github.com/onsi/gomega" //nolint:stylecheck + + "sigs.k8s.io/kustomize/kyaml/yaml" + + fcmd "kraftkit.sh/test/e2e/framework/cmd" + fcfg "kraftkit.sh/test/e2e/framework/config" + . "kraftkit.sh/test/e2e/framework/matchers" //nolint:stylecheck +) + +var _ = Describe("kraft pkg", func() { + var cmd *fcmd.Cmd + + var stdout *fcmd.IOStream + var stderr *fcmd.IOStream + + var cfg *fcfg.Config + + BeforeEach(func() { + stdout = fcmd.NewIOStream() + stderr = fcmd.NewIOStream() + + cfg = fcfg.NewTempConfig() + + cmd = fcmd.NewKraft(stdout, stderr, cfg.Path()) + cmd.Args = append(cmd.Args, "pkg") + }) + + _ = Describe("update", func() { + var manifestsPath string + + BeforeEach(func() { + cmd.Args = append(cmd.Args, "update") + + manifestsPath = yaml.GetValue(cfg.Read("paths", "manifests")) + Expect(manifestsPath).To(SatisfyAny( + Not(BeAnExistingFile()), + BeAnEmptyDirectory(), + ), "manifests directory should either be empty or not yet created") + }) + + Context("implicitly using the default manager type (manifest)", func() { + When("invoked without flags or positional arguments", func() { + It("should retrieve the list of components, libraries and packages", func() { + err := cmd.Run() + Expect(err).ToNot(HaveOccurred()) + + Expect(stderr.String()).To(BeEmpty()) + // The command sends ANSI escape sequences while updating, such as `\e[2K` (erase entire line). + // References: + // https://www.regular-expressions.info/nonprint.html + // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 + Expect(stdout.String()).To(MatchRegexp(`\x1b\[2K\[\+\] Updating\.\.\. \[\d+\.\d+s\]\r\n`), "Quoted output: %q", stdout) + + Expect(manifestsPath).To(ContainFiles("index.yaml", "unikraft.yaml")) + Expect(manifestsPath).To(ContainDirectories("libs")) + }) + }) + + When("invoked with the --help flag", func() { + BeforeEach(func() { + cmd.Args = append(cmd.Args, "--help") + }) + + It("should print the command's help", func() { + err := cmd.Run() + Expect(err).ToNot(HaveOccurred()) + + Expect(stderr.String()).To(BeEmpty()) + Expect(stdout).To(MatchRegexp(`^Retrieve new lists of Unikraft components, libraries and packages.\n`)) + }) + }) + }) + }) +}) diff --git a/test/e2e/cli/version_test.go b/test/e2e/cli/version_test.go index 5a524b8f2..9f269d34b 100644 --- a/test/e2e/cli/version_test.go +++ b/test/e2e/cli/version_test.go @@ -46,6 +46,7 @@ var _ = Describe("kraft version", func() { // The help subsystem is managed by cobra and fails when top-level flags // are passed, so we ensure to keep only the command name and subcommand // from the original cmd. + // Ref. unikraft/kraftkit#430 cmd.Args = []string{cmd.Args[0], cmd.Args[len(cmd.Args)-1], "--help"} }) diff --git a/test/e2e/framework/matchers/be_empty_dir.go b/test/e2e/framework/matchers/be_empty_dir.go new file mode 100644 index 000000000..55f532a4b --- /dev/null +++ b/test/e2e/framework/matchers/be_empty_dir.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +// beAnEmptyDirectoryMatcher asserts that an existing directory is empty. +type beAnEmptyDirectoryMatcher struct { + err error +} + +var _ types.GomegaMatcher = (*beAnEmptyDirectoryMatcher)(nil) + +func (matcher *beAnEmptyDirectoryMatcher) Match(actual any) (success bool, err error) { + actualDirName, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeAnEmptyDirectory matcher expects a directory path") + } + + dirEntries, err := os.ReadDir(actualDirName) + if err != nil { + matcher.err = fmt.Errorf("reading directory entries: %w", err) + return false, nil + } + + n := len(dirEntries) + hasEntries := n > 0 + + if hasEntries { + matcher.err = fmt.Errorf("directory contains %d entries", n) + } + + return !hasEntries, nil +} + +func (matcher *beAnEmptyDirectoryMatcher) FailureMessage(actual any) string { + return format.Message(actual, fmt.Sprintf("to be an empty directory: %s", matcher.err)) +} + +func (*beAnEmptyDirectoryMatcher) NegatedFailureMessage(actual any) string { + return format.Message(actual, "not be an empty directory") +} diff --git a/test/e2e/framework/matchers/be_empty_dir_test.go b/test/e2e/framework/matchers/be_empty_dir_test.go new file mode 100644 index 000000000..33715f43a --- /dev/null +++ b/test/e2e/framework/matchers/be_empty_dir_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers_test + +import ( + "os" + "testing" + + "kraftkit.sh/test/e2e/framework/matchers" +) + +func TestBeAnEmptyDirectoryMatcher(t *testing.T) { + testCases := []struct { + desc string + files []fileEntry + success bool + }{ + { + desc: "Directory is empty", + files: []fileEntry{}, + success: true, + }, { + desc: "Directory contains a file", + files: []fileEntry{regular("f.txt")}, + success: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + d := newDirectory(t, tc.files...) + + m := matchers.BeAnEmptyDirectory() + success, err := m.Match(d) + if err != nil { + t.Fatal("Failed to run matcher:", err) + } + + if tc.success && !success { + t.Error("Expected the matcher to succeed. Failure was:", m.FailureMessage(d)) + } else if !tc.success && success { + t.Error("Expected the matcher to fail") + } + }) + } + + t.Run("Directory does not exist", func(t *testing.T) { + d := newDirectory(t) + if err := os.RemoveAll(d); err != nil { + t.Fatal("Failed to remove temporary directory:", err) + } + + m := matchers.BeAnEmptyDirectory() + success, err := m.Match(d) + if err != nil { + t.Fatal("Failed to run matcher:", err) + } + + if success { + t.Error("Expected the matcher to fail") + } + }) +} diff --git a/test/e2e/framework/matchers/contain_dirs.go b/test/e2e/framework/matchers/contain_dirs.go new file mode 100644 index 000000000..b41b7082d --- /dev/null +++ b/test/e2e/framework/matchers/contain_dirs.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +// containDirectoriesMatcher asserts that a directory contains sub-directories +// with provided names. +type containDirectoriesMatcher struct { + dirNames []string + err error +} + +var _ types.GomegaMatcher = (*containDirectoriesMatcher)(nil) + +func (matcher *containDirectoriesMatcher) Match(actual any) (success bool, err error) { + actualDirName, ok := actual.(string) + if !ok { + return false, fmt.Errorf("ContainFiles matcher expects a directory path") + } + + dirEntries, err := os.ReadDir(actualDirName) + if err != nil { + matcher.err = fmt.Errorf("reading directory entries: %w", err) + return false, nil + } + + if n, nExpect := len(dirEntries), len(matcher.dirNames); n < nExpect { + matcher.err = fmt.Errorf("directory contains less entries (%d) than provided sub-directories names (%d)", n, nExpect) + return false, nil + } + + for _, fn := range matcher.dirNames { + fi, err := os.Stat(filepath.Join(actualDirName, fn)) + if err != nil { + matcher.err = fmt.Errorf("reading file info: %w", err) + return false, nil + } + + if !fi.IsDir() { + matcher.err = fmt.Errorf("file %q is not a directory (type: %s)", fi.Name(), fi.Mode().Type()) + return false, nil + } + } + + return true, nil +} + +func (matcher *containDirectoriesMatcher) FailureMessage(actual any) string { + return format.Message(actual, fmt.Sprintf("to contain the directories with the provided names: %s", matcher.err)) +} + +func (*containDirectoriesMatcher) NegatedFailureMessage(actual any) string { + return format.Message(actual, "not contain the directories with the provided names") +} diff --git a/test/e2e/framework/matchers/contain_dirs_test.go b/test/e2e/framework/matchers/contain_dirs_test.go new file mode 100644 index 000000000..191ad5990 --- /dev/null +++ b/test/e2e/framework/matchers/contain_dirs_test.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers_test + +import ( + "testing" + + "kraftkit.sh/test/e2e/framework/matchers" +) + +func TestContainDirectoriesMatcher(t *testing.T) { + const d1 = "d1" + const d2 = "d2" + + testCases := []struct { + desc string + files []fileEntry + success bool + }{ + { + desc: "All directories exist", + files: []fileEntry{dir(d1), dir(d2), dir("other")}, + success: true, + }, { + desc: "One directory is missing", + files: []fileEntry{dir(d1), dir("other1"), dir("other2")}, + success: false, + }, { + desc: "All directories are missing", + files: []fileEntry{dir("other1"), dir("other2")}, + success: false, + }, { + desc: "One file is regular", + files: []fileEntry{dir(d1), regular(d2)}, + success: false, + }, { + desc: "One file is a symbolic link", + files: []fileEntry{dir(d1), symlink(d2)}, + success: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + d := newDirectory(t, tc.files...) + + m := matchers.ContainDirectories(d1, d2) + success, err := m.Match(d) + if err != nil { + t.Fatal("Failed to run matcher:", err) + } + + if tc.success && !success { + t.Error("Expected the matcher to succeed. Failure was:", m.FailureMessage(d)) + } else if !tc.success && success { + t.Error("Expected the matcher to fail") + } + }) + } +} diff --git a/test/e2e/framework/matchers/contain_files.go b/test/e2e/framework/matchers/contain_files.go new file mode 100644 index 000000000..090e92fe4 --- /dev/null +++ b/test/e2e/framework/matchers/contain_files.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +// containFilesMatcher asserts that a directory contains files with provided +// names. +type containFilesMatcher struct { + fileNames []string + err error +} + +var _ types.GomegaMatcher = (*containFilesMatcher)(nil) + +func (matcher *containFilesMatcher) Match(actual any) (success bool, err error) { + actualDirName, ok := actual.(string) + if !ok { + return false, fmt.Errorf("ContainFiles matcher expects a directory path") + } + + dirEntries, err := os.ReadDir(actualDirName) + if err != nil { + matcher.err = fmt.Errorf("reading directory entries: %w", err) + return false, nil + } + + if n, nExpect := len(dirEntries), len(matcher.fileNames); n < nExpect { + matcher.err = fmt.Errorf("directory contains less entries (%d) than provided files names (%d)", n, nExpect) + return false, nil + } + + for _, fn := range matcher.fileNames { + fi, err := os.Stat(filepath.Join(actualDirName, fn)) + if err != nil { + matcher.err = fmt.Errorf("reading file info: %w", err) + return false, nil + } + + if !fi.Mode().IsRegular() { + matcher.err = fmt.Errorf("file %q is not regular (type: %s)", fi.Name(), fi.Mode().Type()) + return false, nil + } + } + + return true, nil +} + +func (matcher *containFilesMatcher) FailureMessage(actual any) string { + return format.Message(actual, fmt.Sprintf("to contain the files with the provided names: %s", matcher.err)) +} + +func (*containFilesMatcher) NegatedFailureMessage(actual any) string { + return format.Message(actual, "not contain the files with the provided names") +} diff --git a/test/e2e/framework/matchers/contain_files_test.go b/test/e2e/framework/matchers/contain_files_test.go new file mode 100644 index 000000000..ed107663e --- /dev/null +++ b/test/e2e/framework/matchers/contain_files_test.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package matchers_test + +import ( + "os" + "path/filepath" + "testing" + + "kraftkit.sh/test/e2e/framework/matchers" +) + +func TestContainFilesMatcher(t *testing.T) { + const f1 = "f1.txt" + const f2 = "f2.txt" + + testCases := []struct { + desc string + files []fileEntry + success bool + }{ + { + desc: "All files exist", + files: []fileEntry{regular(f1), regular(f2), regular("other.txt")}, + success: true, + }, { + desc: "One file is missing", + files: []fileEntry{regular(f1), regular("other1.txt"), regular("other2.txt")}, + success: false, + }, { + desc: "All files are missing", + files: []fileEntry{regular("other1.txt"), regular("other2.txt")}, + success: false, + }, { + desc: "One file is a directory", + files: []fileEntry{regular(f1), dir(f2)}, + success: false, + }, { + desc: "One file is a symbolic link", + files: []fileEntry{regular(f1), symlink(f2)}, + success: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + d := newDirectory(t, tc.files...) + + m := matchers.ContainFiles(f1, f2) + success, err := m.Match(d) + if err != nil { + t.Fatal("Failed to run matcher:", err) + } + + if tc.success && !success { + t.Error("Expected the matcher to succeed. Failure was:", m.FailureMessage(d)) + } else if !tc.success && success { + t.Error("Expected the matcher to fail") + } + }) + } +} + +// newDirectory creates a new temporary directory containing the given files. +// A cleanup function is automatically registered to remove the directory and +// all its children from the filesystem at the end of the given test. +func newDirectory(t *testing.T, files ...fileEntry) (path string) { + t.Helper() + + tmpDir, err := os.MkdirTemp("", "kraftkit-e2e-matchers-*") + if err != nil { + t.Fatal("Failed to create temporary directory:", err) + } + t.Cleanup(func() { + if err := os.RemoveAll(tmpDir); err != nil { + t.Fatal("Failed to remove temporary directory:", err) + } + }) + + for _, fe := range files { + fp := filepath.Join(tmpDir, fe.name) + + switch ft := fe.typ; ft { + case fileTypeRegular: + f, err := os.OpenFile(fp, os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + t.Fatalf("Failed to create regular file %q: %s", fe.name, err) + } + f.Close() + + case fileTypeDirectory: + if err := os.Mkdir(fp, 0o700); err != nil { + t.Fatalf("Failed to create directory %q: %s", fe.name, err) + } + + case fileTypeSymlink: + if err := os.Symlink("/dev/null", fp); err != nil { + t.Fatalf("Failed to create symbolic link %q: %s", fe.name, err) + } + + default: + t.Fatalf("Unknown type %v for file %q", ft, fe.name) + } + } + + return tmpDir +} + +// fileEntry represents a file to create in a directory used for tests. +type fileEntry struct { + name string + typ fileType +} + +// regular returns a fileEntry for a regular file. +func regular(name string) fileEntry { + return fileEntry{name: name, typ: fileTypeRegular} +} + +// dir returns a fileEntry for a directory. +func dir(name string) fileEntry { + return fileEntry{name: name, typ: fileTypeDirectory} +} + +// symlink returns a fileEntry for a symbolic link. +func symlink(name string) fileEntry { + return fileEntry{name: name, typ: fileTypeSymlink} +} + +type fileType uint8 + +const ( + fileTypeUnknown fileType = iota + fileTypeRegular + fileTypeDirectory + fileTypeSymlink +) diff --git a/test/e2e/framework/matchers/matchers.go b/test/e2e/framework/matchers/matchers.go new file mode 100644 index 000000000..0d887b592 --- /dev/null +++ b/test/e2e/framework/matchers/matchers.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +// Package matchers contains additional Gomega matchers. +package matchers + +import "github.com/onsi/gomega/types" + +// BeAnEmptyDirectory succeeds if a file exists and is a directory that does +// not contain any file. +// Actual must be a string representing the absolute path to the directory +// being checked. +func BeAnEmptyDirectory() types.GomegaMatcher { + return &beAnEmptyDirectoryMatcher{} +} + +// ContainFiles succeeds if a directory exists and contains files with the +// provided names. +// Actual must be a string representing the absolute path to the directory +// containing these files. +func ContainFiles(files ...string) types.GomegaMatcher { + return &containFilesMatcher{fileNames: files} +} + +// ContainDirectories succeeds if a directory exists and contains +// sub-directories with the provided names. +// Actual must be a string representing the absolute path to the directory +// containing these sub-directories. +func ContainDirectories(dirs ...string) types.GomegaMatcher { + return &containDirectoriesMatcher{dirNames: dirs} +}