Skip to content

Commit

Permalink
Implement sum test cases and a mocked Executor model
Browse files Browse the repository at this point in the history
  • Loading branch information
splaspood committed Aug 5, 2024
1 parent 678cddf commit 2cfd480
Show file tree
Hide file tree
Showing 12 changed files with 4,268 additions and 29 deletions.
11 changes: 11 additions & 0 deletions fixtures/internal/sum/ChangeBiosConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64)
Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved.
.................


Note: No BIOS setting has been changed.

Status: The BIOS configuration is updated for 10.145.129.168

Note: You have to reboot or power up the system for the changes to take effect.

7 changes: 7 additions & 0 deletions fixtures/internal/sum/ChangeBiosConfig-Changed
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64)
Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved.
..................
Status: The BIOS configuration is updated for 10.145.129.168

Note: You have to reboot or power up the system for the changes to take effect.

16 changes: 16 additions & 0 deletions fixtures/internal/sum/ChangeBiosConfig-Changed-Reboot
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64)
Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved.
.................
Status: The managed system 10.145.129.168 is rebooting.

.............................Done
....
.................


Note: No BIOS setting has been changed.

Status: The BIOS configuration is updated for 10.145.129.168

WARNING: Without option --post_complete, please manually confirm the managed system is POST complete before executing next action.

3,634 changes: 3,634 additions & 0 deletions fixtures/internal/sum/GetBiosConfiguration

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions fixtures/internal/sum/SetBiosConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64)
Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved.
.................
Status: The managed system 10.145.129.168 is rebooting.

.............................Done
....
.................


Note: No BIOS setting has been changed.

Status: The BIOS configuration is updated for 10.145.129.168

WARNING: Without option --post_complete, please manually confirm the managed system is POST complete before executing next action.

44 changes: 44 additions & 0 deletions internal/executor/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package executor

import (
"errors"
"fmt"
)

var (
ErrNoCommandOutput = errors.New("command returned no output")
ErrVersionStrExpectedSemver = errors.New("expected version string to follow semver format")
ErrFakeExecutorInvalidArgs = errors.New("invalid number of args passed to fake executor")
ErrRepositoryBaseURL = errors.New("repository base URL undefined, ensure UpdateOptions.BaseURL OR UPDATE_BASE_URL env var is set")
ErrNoUpdatesApplicable = errors.New("no updates applicable")
ErrDmiDecodeRun = errors.New("error running dmidecode")
ErrComponentListExpected = errors.New("expected a list of components to apply updates")
ErrDeviceInventory = errors.New("failed to collect device inventory")
ErrUnsupportedDiskVendor = errors.New("unsupported disk vendor")
ErrNoUpdateHandlerForComponent = errors.New("component slug has no update handler declared")
ErrBinNotExecutable = errors.New("bin has no executable bit set")
ErrBinLstat = errors.New("failed to run lstat on bin")
ErrBinLookupPath = errors.New("failed to lookup bin path")
)

// ExecError is returned when the command exits with an error or a non zero exit status
type ExecError struct {
Cmd string
Stderr string
Stdout string
ExitCode int
}

// Error implements the error interface
func (u *ExecError) Error() string {
return fmt.Sprintf("cmd %s exited with error: %s\n\t exitCode: %d\n\t stdout: %s", u.Cmd, u.Stderr, u.ExitCode, u.Stdout)
}

func newExecError(cmd string, r *Result) *ExecError {
return &ExecError{
Cmd: cmd,
Stderr: string(r.Stderr),
Stdout: string(r.Stdout),
ExitCode: r.ExitCode,
}
}
168 changes: 168 additions & 0 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package executor

import (
"bytes"
"context"
"io"
"os"
"os/exec"
"strings"

"github.com/pkg/errors"
)

// Executor interface lets us implement dummy executors for tests
type Executor interface {
ExecWithContext(context.Context) (*Result, error)
SetArgs([]string)
SetEnv([]string)
SetQuiet()
SetVerbose()
GetCmd() string
DisableBinCheck()
SetStdin(io.Reader)
CmdPath() string
CheckExecutable() error
// for tests
SetStdout([]byte)
SetStderr([]byte)
SetExitCode(int)
}

func NewExecutor(cmd string) Executor {
return &Execute{Cmd: cmd, CheckBin: true}
}

// An execute instace
type Execute struct {
Cmd string
Args []string
Env []string
Stdin io.Reader
CheckBin bool
Quiet bool
}

// The result of a command execution
type Result struct {
Stdout []byte
Stderr []byte
ExitCode int
}

// GetCmd returns the command with args as a string
func (e *Execute) GetCmd() string {
cmd := []string{e.Cmd}
cmd = append(cmd, e.Args...)

return strings.Join(cmd, " ")
}

// CmdPath returns the absolute path to the executable
// this means the caller should not have disabled CheckBin.
func (e *Execute) CmdPath() string {
return e.Cmd
}

// SetArgs sets the command args
func (e *Execute) SetArgs(a []string) {
e.Args = a
}

// SetEnv sets the env variables
func (e *Execute) SetEnv(env []string) {
e.Env = env
}

// SetQuiet lowers the verbosity
func (e *Execute) SetQuiet() {
e.Quiet = true
}

// SetVerbose does whats it says
func (e *Execute) SetVerbose() {
e.Quiet = false
}

// SetStdin sets the reader to the command stdin
func (e *Execute) SetStdin(r io.Reader) {
e.Stdin = r
}

// DisableBinCheck disables validating the binary exists and is executable
func (e *Execute) DisableBinCheck() {
e.CheckBin = false
}

// SetStdout doesn't do much, is around for tests
func (e *Execute) SetStdout(_ []byte) {
}

// SetStderr doesn't do much, is around for tests
func (e *Execute) SetStderr(_ []byte) {
}

// SetExitCode doesn't do much, is around for tests
func (e *Execute) SetExitCode(_ int) {
}

// ExecWithContext executes the command and returns the Result object
func (e *Execute) ExecWithContext(ctx context.Context) (result *Result, err error) {
if e.CheckBin {
err = e.CheckExecutable()
if err != nil {
return nil, err
}
}

cmd := exec.CommandContext(ctx, e.Cmd, e.Args...)
cmd.Env = append(cmd.Env, e.Env...)
cmd.Stdin = e.Stdin

var stdoutBuf, stderrBuf bytes.Buffer
if !e.Quiet {
cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
} else {
cmd.Stderr = &stderrBuf
cmd.Stdout = &stdoutBuf
}

if err := cmd.Run(); err != nil {
result = &Result{stdoutBuf.Bytes(), stderrBuf.Bytes(), cmd.ProcessState.ExitCode()}
return result, newExecError(e.GetCmd(), result)
}

result = &Result{stdoutBuf.Bytes(), stderrBuf.Bytes(), cmd.ProcessState.ExitCode()}

return result, nil
}

// CheckExecutable determines if the set Cmd value exists as a file and is an executable.
func (e *Execute) CheckExecutable() error {
var path string

if strings.Contains(e.Cmd, "/") {
path = e.Cmd
} else {
var err error
path, err = exec.LookPath(e.Cmd)
if err != nil {
return errors.Wrap(ErrBinLookupPath, err.Error())
}

e.Cmd = path
}

fileInfo, err := os.Lstat(path)
if err != nil {
return errors.Wrap(ErrBinLstat, err.Error())
}

// bit mask 0111 indicates atleast one of owner, group, others has an executable bit set
if fileInfo.Mode()&0o111 == 0 {
return ErrBinNotExecutable
}

return nil
}
117 changes: 117 additions & 0 deletions internal/executor/executor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package executor

import (
"bytes"
"context"
"fmt"
"io/fs"
"os"
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

func Test_Stdin(t *testing.T) {
e := new(Execute)
e.Cmd = "grep"
e.Args = []string{"hello"}
e.Stdin = bytes.NewReader([]byte("hello"))
e.SetQuiet()

result, err := e.ExecWithContext(context.Background())
if err != nil {
fmt.Println(err.Error())
}

assert.Equal(t, []byte("hello\n"), result.Stdout)
}

type checkBinTester struct {
createFile bool
filePath string
expectedErr error
fileMode uint
testName string
}

func initCheckBinTests() []checkBinTester {
return []checkBinTester{
{
false,
"f",
ErrBinLookupPath,
0,
"bin path lookup err test",
},
{
false,
"/tmp/f",
ErrBinLstat,
0,
"bin exists err test",
},
{
true,
"/tmp/f",
ErrBinNotExecutable,
0o666,
"bin exists with no executable bit test",
},
{
true,
"/tmp/j",
nil,
0o667,
"bin with executable bit returns no error",
},
{
true,
"/tmp/k",
nil,
0o700,
"bin with owner executable bit returns no error",
},
{
true,
"/tmp/l",
nil,
0o070,
"bin with group executable bit returns no error",
},
{
true,
"/tmp/m",
nil,
0o007,
"bin with other executable bit returns no error",
},
}
}

func Test_CheckExecutable(t *testing.T) {
tests := initCheckBinTests()
for _, c := range tests {
if c.createFile {
f, err := os.Create(c.filePath)
if err != nil {
t.Error(err)
}

// nolint:gocritic // test code
defer os.Remove(c.filePath)

if c.fileMode != 0 {
err = f.Chmod(fs.FileMode(c.fileMode))
if err != nil {
t.Error(err)
}
}
}

e := new(Execute)
e.Cmd = c.filePath
err := e.CheckExecutable()
assert.Equal(t, c.expectedErr, errors.Cause(err), c.testName)
}
}
Loading

0 comments on commit 2cfd480

Please sign in to comment.