From 15b7a1b7cef54adb36e188a07e55137e420acb7c Mon Sep 17 00:00:00 2001 From: Chandana Mallikarjuna Date: Wed, 16 Jul 2025 12:46:07 +0530 Subject: [PATCH] Add unit tests for files in pkg/cli/alpha/internal/update Add pkg/cli/alpha/internal/update/prepare_test.go Add pkg/cli/alpha/internal/update/update_test.go Add pkg/cli/alpha/internal/update/validate_test.go Add pkg/cli/alpha/internal/update/utils_test.go --- go.mod | 2 + go.sum | 5 + pkg/cli/alpha/internal/update/prepare.go | 2 +- pkg/cli/alpha/internal/update/prepare_test.go | 178 +++++++++ pkg/cli/alpha/internal/update/update.go | 6 +- pkg/cli/alpha/internal/update/update_test.go | 372 ++++++++++++++++++ pkg/cli/alpha/internal/update/utils_test.go | 33 ++ .../alpha/internal/update/validate_test.go | 169 ++++++++ 8 files changed, 763 insertions(+), 4 deletions(-) create mode 100755 pkg/cli/alpha/internal/update/prepare_test.go create mode 100644 pkg/cli/alpha/internal/update/update_test.go create mode 100644 pkg/cli/alpha/internal/update/utils_test.go create mode 100644 pkg/cli/alpha/internal/update/validate_test.go diff --git a/go.mod b/go.mod index c6b610bc9a4..1d6a094e73b 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/h2non/gock v1.2.0 // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index b857daf65be..3be0e6d3cef 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -27,6 +31,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= diff --git a/pkg/cli/alpha/internal/update/prepare.go b/pkg/cli/alpha/internal/update/prepare.go index faf4a24fcb9..0f7261dd5a7 100644 --- a/pkg/cli/alpha/internal/update/prepare.go +++ b/pkg/cli/alpha/internal/update/prepare.go @@ -59,7 +59,7 @@ func (opts *Update) Prepare() error { // defineFromVersion will return the CLI version to be used for the update with the v prefix. func (opts *Update) defineFromVersion(config store.Store) (string, error) { - if len(opts.FromBranch) == 0 && len(config.Config().GetCliVersion()) == 0 { + if len(opts.FromVersion) == 0 && len(config.Config().GetCliVersion()) == 0 { return "", fmt.Errorf("no version specified in PROJECT file. " + "Please use --from-version flag to specify the version to update from") } diff --git a/pkg/cli/alpha/internal/update/prepare_test.go b/pkg/cli/alpha/internal/update/prepare_test.go new file mode 100755 index 00000000000..d0450f05ff4 --- /dev/null +++ b/pkg/cli/alpha/internal/update/prepare_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2025 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 update + +import ( + "os" + "path/filepath" + "testing" + + "github.com/h2non/gock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v4/pkg/cli/alpha/internal/common" + "sigs.k8s.io/kubebuilder/v4/pkg/config" + "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" + v3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" +) + +func TestCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "update") +} + +var _ = Describe("Prepare for internal update", func() { + var ( + tmpDir string + workDir string + projectFile string + err error + ) + + BeforeEach(func() { + workDir, err = os.Getwd() + Expect(err).ToNot(HaveOccurred()) + + tmpDir, err = os.MkdirTemp("", "kubebuilder-prepare-test") + Expect(err).ToNot(HaveOccurred()) + err = os.Chdir(tmpDir) + Expect(err).ToNot(HaveOccurred()) + + projectFile = filepath.Join(tmpDir, yaml.DefaultPath) + + config.Register(config.Version{Number: 3}, func() config.Config { + return &v3.Cfg{Version: config.Version{Number: 3}, CliVersion: "1.0.0"} + }) + + gock.New("https://api.github.com"). + Get("/repos/kubernetes-sigs/kubebuilder/releases/latest"). + Reply(200). + JSON(map[string]string{ + "tag_name": "v1.1.0", + }) + }) + + AfterEach(func() { + err = os.Chdir(workDir) + Expect(err).ToNot(HaveOccurred()) + + err = os.RemoveAll(tmpDir) + Expect(err).ToNot(HaveOccurred()) + defer gock.Off() + }) + + Context("Prepare", func() { + DescribeTable("should succeed for valid options", + func(options *Update) { + const version = `version: "3"` + Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) + + result := options.Prepare() + Expect(result).ToNot(HaveOccurred()) + Expect(options.Prepare()).To(Succeed()) + Expect(options.FromVersion).To(Equal("v1.0.0")) + Expect(options.ToVersion).To(Equal("v1.1.0")) + }, + Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), + Entry("options", &Update{FromVersion: "1.0.0", ToVersion: "1.1.0", FromBranch: "test"}), + Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0"}), + Entry("options", &Update{}), + ) + + DescribeTable("Should fail to prepare if project path is undetermined", + func(options *Update) { + err = options.Prepare() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to determine project path")) + }, + Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), + ) + + DescribeTable("Should fail if PROJECT config could not be loaded", + func(options *Update) { + const version = "" + Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) + + err := options.Prepare() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to load PROJECT config")) + }, + Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), + ) + + DescribeTable("Should fail if FromVersion cannot be determined", + func(options *Update) { + config.Register(config.Version{Number: 3}, func() config.Config { + return &v3.Cfg{Version: config.Version{Number: 3}} + }) + + const version = `version: "3"` + Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) + Expect(options.FromVersion).To(BeEquivalentTo("")) + }, + Entry("options", &Update{}), + ) + }) + + Context("DefineFromVersion", func() { + DescribeTable("Should succeed when --from-version or CliVersion in Project config is present", + func(options *Update) { + const version = `version: "3"` + Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) + + config, err := common.LoadProjectConfig(tmpDir) + Expect(err).ToNot(HaveOccurred()) + fromVersion, err := options.defineFromVersion(config) + Expect(err).ToNot(HaveOccurred()) + Expect(fromVersion).To(BeEquivalentTo("v1.0.0")) + }, + Entry("options", &Update{FromVersion: ""}), + Entry("options", &Update{FromVersion: "1.0.0"}), + ) + DescribeTable("Should fail when --from-version and CliVersion in Project config both are absent", + func(options *Update) { + config.Register(config.Version{Number: 3}, func() config.Config { + return &v3.Cfg{Version: config.Version{Number: 3}} + }) + + const version = `version: "3"` + Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) + + config, err := common.LoadProjectConfig(tmpDir) + Expect(err).NotTo(HaveOccurred()) + fromVersion, err := options.defineFromVersion(config) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no version specified in PROJECT file")) + Expect(fromVersion).To(Equal("")) + }, + Entry("options", &Update{FromVersion: ""}), + ) + }) + + Context("DefineToVersion", func() { + DescribeTable("Should succeed.", + func(options *Update) { + toVersion := options.defineToVersion() + Expect(toVersion).To(BeEquivalentTo("v1.1.0")) + }, + Entry("options", &Update{ToVersion: "1.1.0"}), + Entry("options", &Update{ToVersion: "v1.1.0"}), + Entry("options", &Update{}), + ) + }) +}) diff --git a/pkg/cli/alpha/internal/update/update.go b/pkg/cli/alpha/internal/update/update.go index 212bc333b6d..72de477212c 100644 --- a/pkg/cli/alpha/internal/update/update.go +++ b/pkg/cli/alpha/internal/update/update.go @@ -142,7 +142,7 @@ func (opts *Update) prepareAncestorBranch() error { if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err) } - commitMessage := "Clean scaffold from release version:" + opts.FromVersion + commitMessage := "Clean scaffolding from release version: " + opts.FromVersion _ = exec.Command("git", "commit", "-m", commitMessage).Run() return nil } @@ -252,7 +252,7 @@ func runAlphaGenerate(tempDir, version string) error { if err := cmd.Run(); err != nil { return fmt.Errorf("failed to run alpha generate: %w", err) } - log.Info("Successfully ran alpha generate", version) + log.Info("Successfully ran alpha generate ", version) // TODO: Analyse if this command is still needed in the future. // It was added because the alpha generate command in versions prior to v4.7.0 does @@ -314,7 +314,7 @@ func (opts *Update) prepareUpgradeBranch() error { return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err) } - _ = exec.Command("git", "commit", "-m", "Clean scaffolding from version "+opts.ToVersion).Run() + _ = exec.Command("git", "commit", "-m", "Clean scaffolding from release version: "+opts.ToVersion).Run() return nil } diff --git a/pkg/cli/alpha/internal/update/update_test.go b/pkg/cli/alpha/internal/update/update_test.go new file mode 100644 index 00000000000..f8bb9d86a63 --- /dev/null +++ b/pkg/cli/alpha/internal/update/update_test.go @@ -0,0 +1,372 @@ +/* +Copyright 2025 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 update + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/h2non/gock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Mock response for binary executables +func mockBinResponse(script, mockBin string) error { + err := os.WriteFile(mockBin, []byte(script), 0o755) + Expect(err).NotTo(HaveOccurred()) + if err != nil { + return fmt.Errorf("Error Mocking bin response: %w", err) + } + return nil +} + +// Mock response from an url +func mockURLResponse(body, url string, times, reply int) { + urlStrings := strings.Split(url, "/") + gockNew := strings.Join(urlStrings[0:3], "/") + get := "/" + strings.Join(urlStrings[3:], "/") + gock.New(gockNew). + Get(get). + Times(times). + Reply(reply). + Body(strings.NewReader(body)) +} + +var _ = Describe("Prepare for internal update", func() { + var ( + tmpDir string + mockGit string + mockMake string + mocksh string + logFile string + oldPath string + err error + opts Update + ) + + BeforeEach(func() { + opts = Update{ + FromVersion: "v4.5.0", + ToVersion: "v4.6.0", + FromBranch: "main", + } + + // Create temporary directory to house fake bin executables + tmpDir, err = os.MkdirTemp("", "temp-bin") + Expect(err).NotTo(HaveOccurred()) + + // Create a common file to log the command runs from the fake bin + logFile = filepath.Join(tmpDir, "bin.log") + + // Create fake bin executables + mockGit = filepath.Join(tmpDir, "git") + mockMake = filepath.Join(tmpDir, "make") + mocksh = filepath.Join(tmpDir, "sh") + script := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 0` + err = mockBinResponse(script, mockGit) + Expect(err).NotTo(HaveOccurred()) + err = mockBinResponse(script, mockMake) + Expect(err).NotTo(HaveOccurred()) + err = mockBinResponse(script, mocksh) + Expect(err).NotTo(HaveOccurred()) + + // Prepend temp bin directory to PATH env + oldPath = os.Getenv("PATH") + err = os.Setenv("PATH", tmpDir+":"+oldPath) + Expect(err).NotTo(HaveOccurred()) + + // Mock response from "https://github.com/kubernetes-sigs/kubebuilder/releases/download" + mockURLResponse(script, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 200) + }) + + AfterEach(func() { + _ = os.RemoveAll(tmpDir) + _ = os.Setenv("PATH", oldPath) + defer gock.Off() + }) + + Context("Update", func() { + It("Should scucceed updating project using a default three-way Git merge", func() { + err = opts.Update() + Expect(err).ToNot(HaveOccurred()) + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout %s", opts.FromBranch)) + }) + It("Should fail when git command fails", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err = opts.Update() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to checkout base branch %s", opts.FromBranch)) + + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout %s", opts.FromBranch)) + }) + It("Should fail when kubebuilder binary could not be downloaded", func() { + gock.Off() + + // mockURLResponse(fakeBinScript, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 401) + gock.New("https://github.com"). + Get("/kubernetes-sigs/kubebuilder/releases/download"). + Times(2). + Reply(401). + Body(strings.NewReader("")) + + err = opts.Update() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to prepare ancestor branch")) + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout %s", opts.FromBranch)) + }) + }) + + Context("RegenerateProjectWithVersion", func() { + It("Should scucceed downloading release binary and running `alpha generate`", func() { + err = regenerateProjectWithVersion(opts.FromBranch) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail downloading release binary", func() { + // mockURLResponse(fakeBinScript, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 401) + gock.Off() + gock.New("https://github.com"). + Get("/kubernetes-sigs/kubebuilder/releases/download"). + Times(2). + Reply(401). + Body(strings.NewReader("")) + + err = regenerateProjectWithVersion(opts.FromBranch) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to download release %s binary", opts.FromBranch)) + }) + + It("Should fail running alpha generate", func() { + // mockURLResponse(fakeBinScript, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 200) + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + gock.Off() + gock.New("https://github.com"). + Get("/kubernetes-sigs/kubebuilder/releases/download"). + Times(2). + Reply(200). + Body(strings.NewReader(fakeBinScript)) + + err = regenerateProjectWithVersion(opts.FromBranch) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to run alpha generate on ancestor branch")) + }) + }) + + verifyLogs := func(newBranch, oldBranch, fromVersion string) { + logs, readErr := os.ReadFile(logFile) + Expect(readErr).NotTo(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout -b %s %s", newBranch, oldBranch)) + Expect(string(logs)).To(ContainSubstring("checkout %s", newBranch)) + Expect(string(logs)).To(ContainSubstring( + "-c find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {}")) + Expect(string(logs)).To(ContainSubstring("alpha generate")) + Expect(string(logs)).To(ContainSubstring("add --all")) + Expect(string(logs)).To(ContainSubstring("commit -m Clean scaffolding from release version: %s", fromVersion)) + } + + Context("PrepareAncestorBranch", func() { + It("Should scucceed to prepare the ancestor branch", func() { + err = opts.prepareAncestorBranch() + Expect(err).ToNot(HaveOccurred()) + verifyLogs(opts.AncestorBranch, opts.FromBranch, opts.FromVersion) + }) + + It("Should fail to prepare the ancestor branch", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err = opts.prepareAncestorBranch() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create %s from %s", opts.AncestorBranch, opts.FromBranch)) + }) + }) + + Context("PrepareUpgradeBranch", func() { + It("Should scucceed PrepareUpgradeBranch", func() { + err = opts.prepareUpgradeBranch() + Expect(err).ToNot(HaveOccurred()) + verifyLogs(opts.UpgradeBranch, opts.AncestorBranch, opts.ToVersion) + }) + + It("Should fail PrepareUpgradeBranch", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err = opts.prepareUpgradeBranch() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "failed to checkout %s branch off %s", opts.UpgradeBranch, opts.AncestorBranch)) + }) + }) + + Context("BinaryWithVersion", func() { + It("Should scucceed to download the specified released version from GitHub releases", func() { + _, err = binaryWithVersion(opts.FromVersion) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail to download the specified released version from GitHub releases", func() { + // mockURLResponse(fakeBinScript, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 401) + gock.Off() + gock.New("https://github.com"). + Get("/kubernetes-sigs/kubebuilder/releases/download"). + Times(2). + Reply(401). + Body(strings.NewReader("")) + + _, err = binaryWithVersion(opts.FromVersion) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to download the binary: HTTP 401")) + }) + }) + + Context("CleanupBranch", func() { + It("Should scucceed executing cleanup command", func() { + err = cleanupBranch() + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail executing cleanup command", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mocksh) + Expect(err).ToNot(HaveOccurred()) + err = cleanupBranch() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to clean up files")) + }) + }) + + Context("RunMakeTargets", func() { + It("Should fail to run make commands", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockMake) + Expect(err).ToNot(HaveOccurred()) + + runMakeTargets() + }) + }) + + Context("RunAlphaGenerate", func() { + It("Should scucceed runAlphaGenerate", func() { + mockKubebuilder := filepath.Join(tmpDir, "kubebuilder") + KubebuilderScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 0` + err = mockBinResponse(KubebuilderScript, mockKubebuilder) + Expect(err).NotTo(HaveOccurred()) + + err = runAlphaGenerate(tmpDir, opts.FromBranch) + Expect(err).ToNot(HaveOccurred()) + + logs, readErr := os.ReadFile(logFile) + Expect(readErr).NotTo(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("alpha generate")) + }) + + It("Should fail runAlphaGenerate", func() { + mockKubebuilder := filepath.Join(tmpDir, "kubebuilder") + KubebuilderScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(KubebuilderScript, mockKubebuilder) + Expect(err).NotTo(HaveOccurred()) + + err = runAlphaGenerate(tmpDir, opts.FromBranch) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to run alpha generate")) + }) + }) + + Context("PrepareOriginalBranch", func() { + It("Should scucceed prepareOriginalBranch", func() { + err = opts.prepareOriginalBranch() + Expect(err).ToNot(HaveOccurred()) + + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout -b %s", opts.OriginalBranch)) + Expect(string(logs)).To(ContainSubstring("checkout %s -- .", opts.FromBranch)) + Expect(string(logs)).To(ContainSubstring("add --all")) + Expect(string(logs)).To(ContainSubstring( + "commit -m Add code from %s into %s", opts.FromBranch, opts.OriginalBranch)) + }) + + It("Should fail prepareOriginalBranch", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err = opts.prepareOriginalBranch() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to checkout branch %s", opts.OriginalBranch)) + }) + }) + + Context("MergeOriginalToUpgrade", func() { + It("Should scucceed MergeOriginalToUpgrade", func() { + err = opts.mergeOriginalToUpgrade() + Expect(err).ToNot(HaveOccurred()) + + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("checkout -b %s %s", opts.MergeBranch, opts.UpgradeBranch)) + Expect(string(logs)).To(ContainSubstring("checkout %s", opts.MergeBranch)) + Expect(string(logs)).To(ContainSubstring("merge --no-edit --no-commit %s", opts.OriginalBranch)) + Expect(string(logs)).To(ContainSubstring("add --all")) + Expect(string(logs)).To(ContainSubstring("Merge from %s to %s.", opts.FromVersion, opts.ToVersion)) + Expect(string(logs)).To(ContainSubstring("Merge happened without conflicts")) + }) + + It("Should fail MergeOriginalToUpgrade", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err := opts.mergeOriginalToUpgrade() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "failed to create merge branch %s from %s", opts.MergeBranch, opts.OriginalBranch)) + }) + }) +}) diff --git a/pkg/cli/alpha/internal/update/utils_test.go b/pkg/cli/alpha/internal/update/utils_test.go new file mode 100644 index 00000000000..921ab9e678f --- /dev/null +++ b/pkg/cli/alpha/internal/update/utils_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2025 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 update + +import ( + "runtime" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("utils", func() { + Context("BuildReleaseURL", func() { + It("Should scucceed", func() { + output := buildReleaseURL("v4.5.0") + Expect(output).To(ContainSubstring(releaseURL, "v4.5.0", runtime.GOOS, runtime.GOARCH)) + }) + }) +}) diff --git a/pkg/cli/alpha/internal/update/validate_test.go b/pkg/cli/alpha/internal/update/validate_test.go new file mode 100644 index 00000000000..76a26e143bc --- /dev/null +++ b/pkg/cli/alpha/internal/update/validate_test.go @@ -0,0 +1,169 @@ +/* +Copyright 2025 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 update + +import ( + "os" + "path/filepath" + "strings" + + "github.com/h2non/gock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Prepare for internal update", func() { + var ( + tmpDir string + mockGit string + logFile string + oldPath string + err error + opts *Update + ) + + BeforeEach(func() { + opts = &Update{ + FromVersion: "v4.5.0", + ToVersion: "v4.6.0", + FromBranch: "main", + OriginalBranch: "v4.6.0", + } + + // Create temporary directory to house fake bin executables + tmpDir, err = os.MkdirTemp("", "temp-bin") + Expect(err).NotTo(HaveOccurred()) + + // Create a common file to log the command runs from the fake bin + logFile = filepath.Join(tmpDir, "bin.log") + + // Create fake bin executables + mockGit = filepath.Join(tmpDir, "git") + script := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 0` + err = mockBinResponse(script, mockGit) + Expect(err).NotTo(HaveOccurred()) + + // Prepend temp bin directory to PATH env + oldPath = os.Getenv("PATH") + err = os.Setenv("PATH", tmpDir+":"+oldPath) + Expect(err).NotTo(HaveOccurred()) + + gock.New("https://github.com"). + Head("/kubernetes-sigs/kubebuilder/releases/download"). + Times(2). + Reply(200). + Body(strings.NewReader("body")) + }) + + AfterEach(func() { + _ = os.RemoveAll(tmpDir) + _ = os.Setenv("PATH", oldPath) + defer gock.Off() + }) + + Context("Validate", func() { + It("Should scucceed", func() { + err = opts.Validate() + Expect(err).ToNot(HaveOccurred()) + }) + It("Should fail", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + + err = opts.Validate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to validate git repository")) + }) + }) + + Context("ValidateGitRepo", func() { + It("Should scucceed", func() { + err = opts.validateGitRepo() + Expect(err).ToNot(HaveOccurred()) + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("rev-parse --git-dir")) + Expect(string(logs)).To(ContainSubstring("status --porcelain")) + }) + It("Should fail", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + + err = opts.validateGitRepo() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in a git repository")) + }) + }) + + Context("ValidateFromBranch", func() { + It("Should scucceed", func() { + err = opts.validateFromBranch() + Expect(err).ToNot(HaveOccurred()) + logs, readErr := os.ReadFile(logFile) + Expect(readErr).ToNot(HaveOccurred()) + Expect(string(logs)).To(ContainSubstring("rev-parse --verify %s", opts.FromBranch)) + }) + It("Should fail", func() { + fakeBinScript := `#!/bin/bash + echo "$@" >> "` + logFile + `" + exit 1` + err = mockBinResponse(fakeBinScript, mockGit) + Expect(err).ToNot(HaveOccurred()) + err := opts.validateFromBranch() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("branch does not exist locally")) + }) + }) + + Context("ValidateSemanticVersions", func() { + It("Should scucceed", func() { + err := opts.validateSemanticVersions() + Expect(err).ToNot(HaveOccurred()) + }) + It("Should fail", func() { + opts.FromVersion = "6" + err := opts.validateSemanticVersions() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("has invalid semantic version. Expect: vX.Y.Z")) + }) + }) + + Context("ValidateReleaseAvailability", func() { + It("Should scucceed", func() { + err := validateReleaseAvailability(opts.ToVersion) + Expect(err).ToNot(HaveOccurred()) + }) + It("Should fail", func() { + gock.Off() + gock.New("https://github.com"). + Head("/kubernetes-sigs/kubebuilder/releases/download"). + Reply(401). + Body(strings.NewReader("body")) + err := validateReleaseAvailability(opts.FromVersion) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected response")) + }) + }) +})