From b549fc1821b93dbbec8bb9204b95e64d4ec1b0a0 Mon Sep 17 00:00:00 2001 From: Trevor Dawe Date: Tue, 4 Feb 2025 00:07:59 +0000 Subject: [PATCH] Updated unit tests for semver package --- core/semver/semver.go | 132 +++++++++++------- core/semver/semver_test.go | 273 +++++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 core/semver/semver_test.go diff --git a/core/semver/semver.go b/core/semver/semver.go index 49d794a1..007dd303 100644 --- a/core/semver/semver.go +++ b/core/semver/semver.go @@ -32,30 +32,46 @@ import ( "time" ) +var ( + format string + output string + export bool + tpl *template.Template +) + +func init() { + if flag.Lookup("f") == nil { + flag.StringVar( + &format, + "f", + "ver", + "The output format: env, go, json, mk, rpm, ver") + } + if flag.Lookup("o") == nil { + flag.StringVar( + &output, + "o", + "", + "The output file") + } + if flag.Lookup("x") == nil { + flag.BoolVar( + &export, + "x", + false, + "Export env vars. Used with -f env") + } +} + +func initFlags() { + format = flag.Lookup("f").Value.(flag.Getter).Get().(string) + output = flag.Lookup("o").Value.(flag.Getter).Get().(string) + export = flag.Lookup("x").Value.(flag.Getter).Get().(bool) +} + func main() { - var ( - tpl *template.Template - format string - output string - export bool - ) - - flag.StringVar( - &format, - "f", - "ver", - "The output format: env, go, json, mk, rpm, ver") - flag.StringVar( - &output, - "o", - "", - "The output file") - flag.BoolVar( - &export, - "x", - false, - "Export env vars. Used with -f env") flag.Parse() + initFlags() if strings.EqualFold("env", format) { format = "env" @@ -71,10 +87,9 @@ func main() { format = "ver" } else { if fileExists(format) { - buf, err := os.ReadFile(format) // #nosec G304 + buf, err := ReadFile(format) // #nosec G304 if err != nil { - fmt.Fprintf(os.Stderr, "error: read tpl failed: %v\n", err) - os.Exit(1) + errorExit(fmt.Sprintf("error: read tpl failed: %v\n", err)) } format = string(buf) } @@ -86,8 +101,7 @@ func main() { if len(output) > 0 { fout, err := os.Create(filepath.Clean(output)) if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + errorExit(fmt.Sprintf("error: %v\n", err)) } w = fout defer func() { @@ -102,8 +116,7 @@ func main() { `^[^\d]*(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z].+?))?(?:-(\d+)-g(.+?)(?:-(dirty))?)?\s*$`) m := rx.FindStringSubmatch(gitdesc) if len(m) == 0 { - fmt.Fprintf(os.Stderr, "error: match git describe failed: %s\n", gitdesc) - os.Exit(1) + errorExit(fmt.Sprintf("error: match git describe failed: %s\n", gitdesc)) } goos := os.Getenv("XGOOS") @@ -158,8 +171,7 @@ func main() { enc := json.NewEncoder(w) enc.SetIndent("", " ") if err := enc.Encode(ver); err != nil { - fmt.Fprintf(os.Stderr, "error: encode to json failed: %v\n", err) - os.Exit(1) + errorExit(fmt.Sprintf("error: encode to json failed: %v\n", err)) } case "mk": for _, v := range ver.EnvVars() { @@ -168,22 +180,21 @@ func main() { fmt.Fprintf(w, "%s ?=", key) if len(p) == 1 { fmt.Fprintln(w) - continue + } else { + val := p[1] + if strings.HasPrefix(val, `"`) && + strings.HasSuffix(val, `"`) { + val = val[1 : len(val)-1] + } + val = strings.Replace(val, "$", "$$", -1) + fmt.Fprintf(w, " %s\n", val) } - val := p[1] - if strings.HasPrefix(val, `"`) && - strings.HasSuffix(val, `"`) { - val = val[1 : len(val)-1] - } - val = strings.Replace(val, "$", "$$", -1) - fmt.Fprintf(w, " %s\n", val) } case "rpm": fmt.Fprintln(w, ver.RPM()) case "tpl": if err := tpl.Execute(w, ver); err != nil { - fmt.Fprintf(os.Stderr, "error: template failed: %v\n", err) - os.Exit(1) + errorExit(fmt.Sprintf("error: template failed: %v\n", err)) } case "ver": fmt.Fprintln(w, ver.String()) @@ -196,22 +207,27 @@ func doExec(cmd string, args ...string) ([]byte, error) { return c.Output() } +func errorExit(message string) { + fmt.Fprintf(os.Stderr, "%s", message) + OSExit(1) +} + func chkErr(out []byte, err error) string { if err == nil { return strings.TrimSpace(string(out)) } - e, ok := err.(*exec.ExitError) + e, ok := GetExitError(err) if !ok { - os.Exit(1) + OSExit(1) } - st, ok := e.Sys().(syscall.WaitStatus) + status, ok := GetStatusError(e) if !ok { - os.Exit(1) + OSExit(1) } - os.Exit(st.ExitStatus()) + OSExit(status) return "" } @@ -327,3 +343,27 @@ func fileExists(filePath string) bool { } return false } + +// ReadFile is a wrapper around os.ReadFile +var ReadFile = func(file string) ([]byte, error) { + return os.ReadFile(file) // #nosec G304 +} + +// OSExit is a wrapper around os.Exit +var OSExit = func(code int) { + os.Exit(code) +} + +// GetExitError is a wrapper around exec.ExitError +var GetExitError = func(err error) (e *exec.ExitError, ok bool) { + e, ok = err.(*exec.ExitError) + return +} + +// GetStatusError is a wrapper around syscall.WaitStatus +var GetStatusError = func(exitError *exec.ExitError) (status int, ok bool) { + if e, ok := exitError.Sys().(syscall.WaitStatus); ok { + return e.ExitStatus(), true + } + return 1, false +} diff --git a/core/semver/semver_test.go b/core/semver/semver_test.go new file mode 100644 index 00000000..e7337a8b --- /dev/null +++ b/core/semver/semver_test.go @@ -0,0 +1,273 @@ +/* + Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. + + 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 main + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMainFunction(t *testing.T) { + tests := []struct { + name string + format string + outputFile string + expectEmptyFile bool + readFileFunc func(file string) ([]byte, error) + }{ + { + name: "Write mk format to file", + format: "mk", + outputFile: "test_output.mk", + }, + { + name: "Write env format to file", + format: "env", + outputFile: "test_output.env", + }, + { + name: "Write json format to file", + format: "json", + outputFile: "test_output.json", + }, + { + name: "Write ver format to file", + format: "ver", + outputFile: "test_output.ver", + }, + { + name: "Write rpm format to file", + format: "rpm", + outputFile: "test_output.rpm", + }, + { + name: "Write tpl format to file", + format: "../semver.tpl", + outputFile: "test_output.rpm", + }, + { + name: "Write tpl format to file but error reading source file", + format: "../semver.tpl", + outputFile: "test_output.rpm", + readFileFunc: func(_ string) ([]byte, error) { + return nil, errors.New("error reading source file") + }, + expectEmptyFile: true, + }, + { + // go format currently does not print any output, expect an empty file + name: "Write go format to file", + format: "go", + outputFile: "test_output.go", + expectEmptyFile: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := os.Args + os.Args = append(os.Args, "-f", tt.format) + os.Args = append(os.Args, "-o", tt.outputFile) + os.Args = append(os.Args, "-x", "true") + + oldReadFile := ReadFile + if tt.readFileFunc != nil { + ReadFile = tt.readFileFunc + } + oldOSExit := OSExit + OSExit = func(_ int) {} + + main() + + // Open the file + file, err := os.Open(tt.outputFile) + if err != nil { + t.Error(err) + } + defer file.Close() + + // Read the file contents + contents, err := io.ReadAll(file) + if err != nil { + t.Error(err) + } + + defer os.Remove(tt.outputFile) + + // make sure file is not empty + if tt.expectEmptyFile { + assert.Equal(t, 0, len(contents)) + } else { + assert.NotEqual(t, 0, len(contents)) + } + os.Args = osArgs + ReadFile = oldReadFile + OSExit = oldOSExit + }) + } +} + +func TestChkErr(t *testing.T) { + tests := []struct { + name string + out []byte + err error + wantOut string + wantErr bool + getExitError func(err error) (*exec.ExitError, bool) + getStatusError func(exitError *exec.ExitError) (int, bool) + }{ + { + name: "No error", + out: []byte("output"), + err: nil, + wantOut: "output", + wantErr: false, + getExitError: func(_ error) (*exec.ExitError, bool) { + return nil, true + }, + getStatusError: func(_ *exec.ExitError) (int, bool) { + return 0, true + }, + }, + { + name: "Error with command", + out: []byte("output"), + err: errors.New("error"), + wantOut: "", + wantErr: true, + getExitError: func(_ error) (*exec.ExitError, bool) { + return nil, false + }, + getStatusError: func(_ *exec.ExitError) (int, bool) { + return 1, false + }, + }, + { + name: "Error casting to ExitError", + out: []byte("output"), + err: errors.New("error"), + wantOut: "", + wantErr: true, + getExitError: func(_ error) (*exec.ExitError, bool) { + return nil, true + }, + getStatusError: func(_ *exec.ExitError) (int, bool) { + return 1, false + }, + }, + { + name: "Error getting status from ExitError", + out: []byte("output"), + err: errors.New("error"), + wantOut: "", + wantErr: true, + getExitError: func(_ error) (*exec.ExitError, bool) { + return nil, false + }, + getStatusError: func(_ *exec.ExitError) (int, bool) { + return 0, true + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + GetExitError = tt.getExitError + GetStatusError = tt.getStatusError + OSExit = func(_ int) {} + + gotOut := chkErr(tt.out, tt.err) + if gotOut != tt.wantOut { + t.Errorf("chkErr() gotOut = %v, want %v", gotOut, tt.wantOut) + } + }) + } +} + +func TestFileExists(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "File exists", + filePath: "semver.go", + want: true, + }, + { + name: "File does not exist", + filePath: "non-existent.txt", + want: false, + }, + { + name: "File path is empty", + filePath: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fileExists(tt.filePath) + if got != tt.want { + t.Errorf("fileExists(%s) = %v, want %v", tt.filePath, got, tt.want) + } + }) + } +} + +func TestErrorExit(t *testing.T) { + message := "error message" + + if os.Getenv("INVOKE_ERROR_EXIT") == "1" { + errorExit(message) + return + } + // call the test again with INVOKE_ERROR_EXIT=1 so the errorExit function is invoked and we can check the return code + cmd := exec.Command(os.Args[0], "-test.run=TestErrorExit") // #nosec G204 + cmd.Env = append(os.Environ(), "INVOKE_ERROR_EXIT=1") + + stderr, err := cmd.StderrPipe() + if err != nil { + fmt.Println("Error creating stderr pipe:", err) + return + } + + if err := cmd.Start(); err != nil { + t.Error(err) + } + + buf := make([]byte, 1024) + n, err := stderr.Read(buf) + if err != nil { + t.Error(err) + } + + err = cmd.Wait() + if e, ok := err.(*exec.ExitError); ok && e.Success() { + t.Error(err) + } + + // check the output is the message we logged in errorExit + assert.Equal(t, message, string(buf[:n])) +}