diff --git a/pkg/checks/disk/fio.go b/pkg/checks/disk/fio.go deleted file mode 100644 index 89a4a37..0000000 --- a/pkg/checks/disk/fio.go +++ /dev/null @@ -1,117 +0,0 @@ -package disk - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "strings" -) - -// FioResult is a data structure that represents the JSON -// output returned from running `fio`. -type FioResult struct { - Version string `json:"fio version"` - Jobs []FioJobResult `json:"jobs"` -} - -// FioJobResult represents the results from an individual fio job. A FioResult -// may include multiple job results. -type FioJobResult struct { - Sync FioJobModeResult `json:"sync"` - Read FioJobModeResult `json:"read"` - Write FioJobModeResult `json:"write"` -} - -// FioJobModeResult represents the measurements for a given test mode -// (e.g. read, write). Not all modes provide all values. The populated -// values depend on the fio job parameters. -type FioJobModeResult struct { - Iops float64 `json:"iops"` - IopsMin int64 `json:"iops_min"` - IopsMax int64 `json:"iops_max"` - IopsMean float64 `json:"iops_mean"` - IopsStddev float64 `json:"iops_stddev"` - - LatNs FioResultStats `json:"lat_ns"` -} - -// FioResultStats represents the statistical measurements provided by fio. -type FioResultStats struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - StdDev float64 `json:"stddev"` - N int64 `json:"N"` - Percentile FioPercentile `json:"percentile"` -} - -// FioPercentile provides a simple interface to return particular statistical -// percentiles from the fio stats results. -type FioPercentile struct { - NinetyNinth int64 `json:"99.000000"` -} - -const fioExecutable = "fio" - -func runFio( - jobName string, - args []string, -) (*FioResult, error) { - - fioPath, err := exec.LookPath(fioExecutable) - - if err != nil { - return nil, err - } - - // Create test directory and defer its removal to make sure it's not left - // behind. - err = os.MkdirAll(jobName, os.ModePerm) - if err != nil { - return nil, err - } - defer os.RemoveAll(jobName) - - cmd := exec.Command( - fioPath, - args..., - ) - - var outBuffer, errBuffer bytes.Buffer - cmd.Stdout = &outBuffer - cmd.Stderr = &errBuffer - - err = cmd.Run() // and wait - if err != nil { - return nil, errors.New( - strings.ReplaceAll( - strings.TrimSpace( - errBuffer.String(), - ), - "\n", - ", ", - ), - ) - } - - fioResult := FioResult{} - - jsonBytes := outBuffer.Bytes() - - // Attempt to write the full fio result to a file - outputFilename := fmt.Sprintf("%s.json", jobName) - err = os.WriteFile(outputFilename, jsonBytes, 0644) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to write %s: %s", outputFilename, err) - } - - err = json.Unmarshal(jsonBytes, &fioResult) - if err != nil { - return nil, err - } - - return &fioResult, nil -} diff --git a/pkg/checks/disk/fio/command.go b/pkg/checks/disk/fio/command.go new file mode 100644 index 0000000..c5d1d41 --- /dev/null +++ b/pkg/checks/disk/fio/command.go @@ -0,0 +1,19 @@ +package fio + +type Command interface { + Run() (Result, error) +} + +type ConcreteCommand struct { + args []string +} + +func NewCommand(args ...string) Command { + return &ConcreteCommand{ + args: args, + } +} + +func (*ConcreteCommand) Run() (Result, error) { + return Result{}, nil +} diff --git a/pkg/checks/disk/fio/exec.go b/pkg/checks/disk/fio/exec.go new file mode 100644 index 0000000..929c691 --- /dev/null +++ b/pkg/checks/disk/fio/exec.go @@ -0,0 +1,149 @@ +package fio + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/conjurinc/conjur-preflight/pkg/log" + "github.com/conjurinc/conjur-preflight/pkg/maybe" +) + +const fioExecutable = "fio" + +type commandWrapper struct { + command *exec.Cmd + stdout bytes.Buffer + stderr bytes.Buffer +} + +func newCommandWrapper(name string, args ...string) *commandWrapper { + wrapper := commandWrapper{} + + // Instantiate the command + command := exec.Command(name, args...) + + // Bind the stdout and stderr + command.Stdout = &wrapper.stdout + command.Stderr = &wrapper.stderr + + // Wrap the command + wrapper.command = command + + return &wrapper +} + +func (wrapper *commandWrapper) Run() ([]byte, error) { + err := wrapper.command.Run() + + if err != nil { + log.Error("Failed to run command: %s", err) + return nil, newErrorFromStderr(&wrapper.stderr) + } + + return wrapper.stdout.Bytes(), nil +} + +func Exec( + jobName string, + args []string, +) (*Result, error) { + // Create the directory for running the fio test within + cleanup, err := usingTestDirectory(jobName) + if err != nil { + return nil, fmt.Errorf("unable to create test directory: %s", err) + } + defer cleanup() + + // Run the fio command + maybeOutput := maybeJobOutput(args) + maybeWriteResultToFile(maybeOutput, jobName) + return maybeResultFromJson(maybeOutput).ValueE() +} + +func maybeJobOutput(args []string) maybe.Maybe[[]byte] { + return maybe.Bind( + maybe.Bind( + maybe.Result(exec.LookPath(fioExecutable)), + func(fioPath string) (*commandWrapper, error) { + return newCommandWrapper(fioPath, args...), nil + }, + ), + func(commandWrapper *commandWrapper) ([]byte, error) { + return commandWrapper.Run() + }, + ) +} + +func usingTestDirectory(jobName string) (func(), error) { + err := os.MkdirAll(jobName, os.ModePerm) + if err != nil { + return nil, err + } + + return func() { + err := os.RemoveAll(jobName) + if err != nil { + log.Warn("Unable to clean up test directory for job: %s", jobName) + } + }, nil +} + +func maybeWriteResultToFile(maybeBuffer maybe.Maybe[[]byte], jobName string) { + // Attempt to write the full fio result to a file + maybe.BindVoid( + maybeBuffer, + func(resultBytes []byte) error { + return writeResultToFile(resultBytes, jobName) + }, + ) +} + +func writeResultToFile(buffer []byte, jobName string) error { + + outputFilename := fmt.Sprintf("%s.json", jobName) + + err := os.WriteFile(outputFilename, buffer, 0644) + + if err != nil { + log.Warn("Failed to write result file for %s: %s", jobName, err) + } + + return err +} + +func maybeResultFromJson(maybeBuffer maybe.Maybe[[]byte]) maybe.Maybe[*Result] { + return maybe.Bind( + maybeBuffer, + func(resultBytes []byte) (*Result, error) { + return newResultFromJson(resultBytes) + }, + ) +} + +func newResultFromJson(buffer []byte) (*Result, error) { + result := Result{} + + err := json.Unmarshal(buffer, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func newErrorFromStderr(buffer *bytes.Buffer) error { + str := buffer.String() + + // Trim any extra space at the start or end (e.g. trailing newline) + str = strings.TrimSpace(str) + + // Convert multi-line response into comma separated + str = strings.ReplaceAll(str, "\n", ", ") + + return errors.New(str) +} diff --git a/pkg/checks/disk/fio/types.go b/pkg/checks/disk/fio/types.go new file mode 100644 index 0000000..2cb9a6d --- /dev/null +++ b/pkg/checks/disk/fio/types.go @@ -0,0 +1,45 @@ +package fio + +// Result is a data structure that represents the JSON +// output returned from running `fio`. +type Result struct { + Version string `json:"fio version"` + Jobs []JobResult `json:"jobs"` +} + +// JobResult represents the results from an individual fio job. A FioResult +// may include multiple job results. +type JobResult struct { + Sync JobModeResult `json:"sync"` + Read JobModeResult `json:"read"` + Write JobModeResult `json:"write"` +} + +// JobModeResult represents the measurements for a given test mode +// (e.g. read, write). Not all modes provide all values. The populated +// values depend on the fio job parameters. +type JobModeResult struct { + Iops float64 `json:"iops"` + IopsMin int64 `json:"iops_min"` + IopsMax int64 `json:"iops_max"` + IopsMean float64 `json:"iops_mean"` + IopsStddev float64 `json:"iops_stddev"` + + LatNs ResultStats `json:"lat_ns"` +} + +// ResultStats represents the statistical measurements provided by fio. +type ResultStats struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + StdDev float64 `json:"stddev"` + N int64 `json:"N"` + Percentile Percentile `json:"percentile"` +} + +// Percentile provides a simple interface to return particular statistical +// percentiles from the fio stats results. +type Percentile struct { + NinetyNinth int64 `json:"99.000000"` +} diff --git a/pkg/checks/disk/iops.go b/pkg/checks/disk/iops.go index ae5649b..43a0af7 100644 --- a/pkg/checks/disk/iops.go +++ b/pkg/checks/disk/iops.go @@ -4,21 +4,31 @@ import ( "fmt" "os" + "github.com/conjurinc/conjur-preflight/pkg/checks/disk/fio" "github.com/conjurinc/conjur-preflight/pkg/framework" ) // IopsCheck is a pre-flight check to report the read and write IOPs for the // directory in which `conjur-preflight` is run. type IopsCheck struct { + // We inject the fio command execution as a dependency that we can swap for + // unit testing + fioExec func(string, []string) (*fio.Result, error) +} + +func NewIopsCheck() *IopsCheck { + return &IopsCheck{ + fioExec: fio.Exec, + } } // Run executes the IopsCheck by running `fio` and processing its output -func (*IopsCheck) Run() <-chan []framework.CheckResult { +func (iopsCheck *IopsCheck) Run() <-chan []framework.CheckResult { future := make(chan []framework.CheckResult) go func() { - fioResult, err := runFioIopsTest() + fioResult, err := iopsCheck.runFioIopsTest() if err != nil { future <- []framework.CheckResult{ @@ -33,20 +43,34 @@ func (*IopsCheck) Run() <-chan []framework.CheckResult { return } + // Make sure a job exists in the fio results + if len(fioResult.Jobs) < 1 { + future <- []framework.CheckResult{ + { + Title: "FIO IOPs", + Status: framework.STATUS_ERROR, + Value: "N/A", + Message: "No job results returned by 'fio'", + }, + } + + return + } + future <- []framework.CheckResult{ - fioReadIopsResult(fioResult), - fioWriteIopsResult(fioResult), + fioReadIopsResult(&fioResult.Jobs[0]), + fioWriteIopsResult(&fioResult.Jobs[0]), } }() // async return future } -func fioReadIopsResult(fioResult *FioResult) framework.CheckResult { +func fioReadIopsResult(job *fio.JobResult) framework.CheckResult { // 50 iops min from https://etcd.io/docs/v3.3/op-guide/hardware/ status := framework.STATUS_INFO - if fioResult.Jobs[0].Read.Iops < 50 { + if job.Read.Iops < 50 { status = framework.STATUS_WARN } @@ -57,10 +81,10 @@ func fioReadIopsResult(fioResult *FioResult) framework.CheckResult { // Format value valueStr := fmt.Sprintf( "%0.2f (Min: %d, Max: %d, StdDev: %0.2f)", - fioResult.Jobs[0].Read.Iops, - fioResult.Jobs[0].Read.IopsMin, - fioResult.Jobs[0].Read.IopsMax, - fioResult.Jobs[0].Read.IopsStddev, + job.Read.Iops, + job.Read.IopsMin, + job.Read.IopsMax, + job.Read.IopsStddev, ) return framework.CheckResult{ @@ -70,11 +94,11 @@ func fioReadIopsResult(fioResult *FioResult) framework.CheckResult { } } -func fioWriteIopsResult(fioResult *FioResult) framework.CheckResult { +func fioWriteIopsResult(job *fio.JobResult) framework.CheckResult { // 50 iops min from https://etcd.io/docs/v3.3/op-guide/hardware/ status := framework.STATUS_INFO - if fioResult.Jobs[0].Write.Iops < 50 { + if job.Write.Iops < 50 { status = framework.STATUS_WARN } @@ -85,10 +109,10 @@ func fioWriteIopsResult(fioResult *FioResult) framework.CheckResult { // Format value valueStr := fmt.Sprintf( "%0.2f (Min: %d, Max: %d, StdDev: %0.2f)", - fioResult.Jobs[0].Write.Iops, - fioResult.Jobs[0].Write.IopsMin, - fioResult.Jobs[0].Write.IopsMax, - fioResult.Jobs[0].Write.IopsStddev, + job.Write.Iops, + job.Write.IopsMin, + job.Write.IopsMax, + job.Write.IopsStddev, ) return framework.CheckResult{ @@ -98,8 +122,8 @@ func fioWriteIopsResult(fioResult *FioResult) framework.CheckResult { } } -func runFioIopsTest() (*FioResult, error) { - return runFio( +func (iopsCheck *IopsCheck) runFioIopsTest() (*fio.Result, error) { + return iopsCheck.fioExec( "conjur-fio-iops", []string{ "--filename=conjur-fio-iops/data", diff --git a/pkg/checks/disk/iops_test.go b/pkg/checks/disk/iops_test.go new file mode 100644 index 0000000..a62e720 --- /dev/null +++ b/pkg/checks/disk/iops_test.go @@ -0,0 +1,163 @@ +// This can't be in the disk_test package because this requires access to the +// internal fioExec field on LatencyCheck. +package disk + +import ( + "errors" + "regexp" + "testing" + + "github.com/conjurinc/conjur-preflight/pkg/checks/disk/fio" + "github.com/conjurinc/conjur-preflight/pkg/framework" + "github.com/stretchr/testify/assert" +) + +func TestIopsCheck(t *testing.T) { + testCheck := &IopsCheck{ + fioExec: mockIopsFioExec, + } + resultChan := testCheck.Run() + results := <-resultChan + + assert.Equal( + t, + 2, + len(results), + "There are read and write IOPs results present", + ) + + assertReadIopsResult(t, results[0], framework.STATUS_INFO) + assertWriteIopsResult(t, results[1], framework.STATUS_INFO) +} + +func TestIopsCheckWithPoorPerformance(t *testing.T) { + testCheck := &IopsCheck{ + fioExec: mockIopsFioExecWithPoorPerformance, + } + resultChan := testCheck.Run() + results := <-resultChan + + assert.Equal( + t, + 2, + len(results), + "There are read and write IOPs results present", + ) + + assertReadIopsResult(t, results[0], framework.STATUS_WARN) + assertWriteIopsResult(t, results[1], framework.STATUS_WARN) +} + +func TestIopsWithError(t *testing.T) { + testCheck := &IopsCheck{ + fioExec: mockIopsFioExecWithError, + } + resultChan := testCheck.Run() + results := <-resultChan + + // Expect only the error result + assert.Equal(t, 1, len(results)) + + assert.Equal(t, "FIO IOPs", results[0].Title) + assert.Equal(t, framework.STATUS_ERROR, results[0].Status) + assert.Equal(t, "N/A", results[0].Value) + assert.Equal(t, "test error", results[0].Message) +} + +func TestIopsWithNoJobs(t *testing.T) { + testCheck := &IopsCheck{ + fioExec: mockIopsFioExecWithNoJobs, + } + resultChan := testCheck.Run() + results := <-resultChan + + // Expect only the error result + assert.Equal(t, 1, len(results)) + + assert.Equal(t, "FIO IOPs", results[0].Title) + assert.Equal(t, framework.STATUS_ERROR, results[0].Status) + assert.Equal(t, "N/A", results[0].Value) + assert.Equal(t, "No job results returned by 'fio'", results[0].Message) +} + +func assertReadIopsResult( + t *testing.T, + result framework.CheckResult, + expectedStatus string, +) { + assert.Regexp( + t, + regexp.MustCompile(`FIO - read iops \(.+\)`), + result.Title, + ) + assert.Equal(t, expectedStatus, result.Status) + assert.Regexp( + t, + regexp.MustCompile(`.+ \(Min: .+, Max: .+, StdDev: .+\)`), + result.Value, + ) +} + +func assertWriteIopsResult( + t *testing.T, + result framework.CheckResult, + expectedStatus string, +) { + assert.Regexp( + t, + regexp.MustCompile(`FIO - write iops \(.+\)`), + result.Title, + ) + assert.Equal(t, expectedStatus, result.Status) + assert.Regexp( + t, + regexp.MustCompile(`.+ \(Min: .+, Max: .+, StdDev: .+\)`), + result.Value, + ) +} + +func mockIopsFioExec(jobName string, args []string) (*fio.Result, error) { + mockCommandResult := &fio.Result{ + Jobs: []fio.JobResult{ + { + Read: fio.JobModeResult{ + Iops: 50, + }, + Write: fio.JobModeResult{ + Iops: 50, + }, + }, + }, + } + return mockCommandResult, nil +} + +func mockIopsFioExecWithPoorPerformance( + jobName string, + args []string, +) (*fio.Result, error) { + mockCommandResult := &fio.Result{ + Jobs: []fio.JobResult{ + { + Read: fio.JobModeResult{ + Iops: 48, + }, + Write: fio.JobModeResult{ + Iops: 48, + }, + }, + }, + } + return mockCommandResult, nil +} + +func mockIopsFioExecWithError(jobName string, args []string) (*fio.Result, error) { + return nil, errors.New("test error") +} + +func mockIopsFioExecWithNoJobs(jobName string, args []string) (*fio.Result, error) { + mockCommandResult := &fio.Result{ + Jobs: []fio.JobResult{}, + } + return mockCommandResult, nil +} diff --git a/pkg/checks/disk/latency.go b/pkg/checks/disk/latency.go index 4d65b01..e69d424 100644 --- a/pkg/checks/disk/latency.go +++ b/pkg/checks/disk/latency.go @@ -4,21 +4,30 @@ import ( "fmt" "os" + "github.com/conjurinc/conjur-preflight/pkg/checks/disk/fio" "github.com/conjurinc/conjur-preflight/pkg/framework" ) // LatencyCheck is a pre-flight check to report the read, write, and sync // latency for the directory in which `conjur-preflight` is run. type LatencyCheck struct { + // We inject the fio command execution as a dependency that we can swap for + // unit testing + fioExec func(string, []string) (*fio.Result, error) +} + +func NewLatencyCheck() *LatencyCheck { + return &LatencyCheck{ + fioExec: fio.Exec, + } } // Run executes the LatencyCheck by running `fio` and processing its output -func (*LatencyCheck) Run() <-chan []framework.CheckResult { +func (latencyCheck *LatencyCheck) Run() <-chan []framework.CheckResult { future := make(chan []framework.CheckResult) go func() { - - fioResult, err := runFioLatencyTest() + fioResult, err := latencyCheck.runFioLatencyTest() if err != nil { future <- []framework.CheckResult{ @@ -33,19 +42,33 @@ func (*LatencyCheck) Run() <-chan []framework.CheckResult { return } + // Make sure a job exists in the fio results + if len(fioResult.Jobs) < 1 { + future <- []framework.CheckResult{ + { + Title: "FIO Latency", + Status: framework.STATUS_ERROR, + Value: "N/A", + Message: "No job results returned by 'fio'", + }, + } + + return + } + future <- []framework.CheckResult{ - fioReadLatencyResult(fioResult), - fioWriteLatencyResult(fioResult), - fioSyncLatencyResult(fioResult), + fioReadLatencyResult(&fioResult.Jobs[0]), + fioWriteLatencyResult(&fioResult.Jobs[0]), + fioSyncLatencyResult(&fioResult.Jobs[0]), } }() // async return future } -func fioReadLatencyResult(fioResult *FioResult) framework.CheckResult { +func fioReadLatencyResult(jobResult *fio.JobResult) framework.CheckResult { // Convert the nanosecond result to milliseconds for readability - latMs := float64(fioResult.Jobs[0].Read.LatNs.Percentile.NinetyNinth) / 1e6 + latMs := float64(jobResult.Read.LatNs.Percentile.NinetyNinth) / 1e6 latMsStr := fmt.Sprintf("%0.2f ms", latMs) @@ -63,9 +86,9 @@ func fioReadLatencyResult(fioResult *FioResult) framework.CheckResult { } } -func fioWriteLatencyResult(fioResult *FioResult) framework.CheckResult { +func fioWriteLatencyResult(jobResult *fio.JobResult) framework.CheckResult { // Convert the nanosecond result to milliseconds for readability - latMs := float64(fioResult.Jobs[0].Write.LatNs.Percentile.NinetyNinth) / 1e6 + latMs := float64(jobResult.Write.LatNs.Percentile.NinetyNinth) / 1e6 latMsStr := fmt.Sprintf("%0.2f ms", latMs) @@ -83,9 +106,9 @@ func fioWriteLatencyResult(fioResult *FioResult) framework.CheckResult { } } -func fioSyncLatencyResult(fioResult *FioResult) framework.CheckResult { +func fioSyncLatencyResult(jobResult *fio.JobResult) framework.CheckResult { // Convert the nanosecond result to milliseconds for readability - latMs := float64(fioResult.Jobs[0].Sync.LatNs.Percentile.NinetyNinth) / 1e6 + latMs := float64(jobResult.Sync.LatNs.Percentile.NinetyNinth) / 1e6 latMsStr := fmt.Sprintf("%0.2f ms", latMs) @@ -103,8 +126,8 @@ func fioSyncLatencyResult(fioResult *FioResult) framework.CheckResult { } } -func runFioLatencyTest() (*FioResult, error) { - return runFio( +func (latencyCheck *LatencyCheck) runFioLatencyTest() (*fio.Result, error) { + return latencyCheck.fioExec( "conjur-fio-latency", []string{ "--rw=readwrite", diff --git a/pkg/checks/disk/latency_test.go b/pkg/checks/disk/latency_test.go new file mode 100644 index 0000000..8820aaa --- /dev/null +++ b/pkg/checks/disk/latency_test.go @@ -0,0 +1,82 @@ +// This can't be in the disk_test package because this requires access to the +// internal fioExec field on LatencyCheck. +package disk + +import ( + "regexp" + "testing" + + "github.com/conjurinc/conjur-preflight/pkg/checks/disk/fio" + "github.com/conjurinc/conjur-preflight/pkg/framework" + "github.com/stretchr/testify/assert" +) + +func TestLatencyCheck(t *testing.T) { + testCheck := &LatencyCheck{ + fioExec: mockLatencyFioExec, + } + resultChan := testCheck.Run() + results := <-resultChan + + assert.Equal(t, 3, len(results), "There are disk latency results present") + + assertReadLatencyResult(t, results[0]) + assertWriteLatencyResult(t, results[1]) + assertSyncLatencyResult(t, results[2]) + +} + +func assertReadLatencyResult(t *testing.T, result framework.CheckResult) { + assert.Regexp( + t, + regexp.MustCompile(`FIO - read latency \(99%, .+\)`), + result.Title, + ) + assert.Equal(t, framework.STATUS_INFO, result.Status) + assert.Regexp( + t, + regexp.MustCompile(`.+ ms`), + result.Value, + ) +} + +func assertWriteLatencyResult(t *testing.T, result framework.CheckResult) { + assert.Regexp( + t, + regexp.MustCompile(`FIO - write latency \(99%, .+\)`), + result.Title, + ) + assert.Equal(t, framework.STATUS_INFO, result.Status) + assert.Regexp( + t, + regexp.MustCompile(`.+ ms`), + result.Value, + ) +} + +func assertSyncLatencyResult(t *testing.T, result framework.CheckResult) { + assert.Regexp( + t, + regexp.MustCompile(`FIO - sync latency \(99%, .+\)`), + result.Title, + ) + assert.Equal(t, framework.STATUS_INFO, result.Status) + assert.Regexp( + t, + regexp.MustCompile(`.+ ms`), + result.Value, + ) +} + +func mockLatencyFioExec(jobName string, args []string) (*fio.Result, error) { + mockCommandResult := &fio.Result{ + Jobs: []fio.JobResult{ + { + Read: fio.JobModeResult{}, + Write: fio.JobModeResult{}, + Sync: fio.JobModeResult{}, + }, + }, + } + return mockCommandResult, nil +} diff --git a/pkg/checks/disk/space_test.go b/pkg/checks/disk/space_test.go index 12d841c..8ddc35e 100644 --- a/pkg/checks/disk/space_test.go +++ b/pkg/checks/disk/space_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDiskRun(t *testing.T) { +func TestSpaceCheck(t *testing.T) { testCheck := &disk.SpaceCheck{} resultChan := testCheck.Run() results := <-resultChan diff --git a/pkg/maybe/failure.go b/pkg/maybe/failure.go new file mode 100644 index 0000000..88f20fd --- /dev/null +++ b/pkg/maybe/failure.go @@ -0,0 +1,23 @@ +package maybe + +type Failure[A any] struct { + err error +} + +func NewFailure[A any](err error) *Failure[A] { + return &Failure[A]{ + err: err, + } +} + +func (failure *Failure[A]) Error() error { + return failure.err +} + +func (failure *Failure[A]) ValueE() (A, error) { + return *new(A), failure.err +} + +func (failure *Failure[A]) Value() A { + return *new(A) +} diff --git a/pkg/maybe/maybe.go b/pkg/maybe/maybe.go index be3ecf8..f0d6c71 100644 --- a/pkg/maybe/maybe.go +++ b/pkg/maybe/maybe.go @@ -1,15 +1,15 @@ package maybe -import "errors" - -type BindFunc[A, B any] func(A) (B, error) - type Maybe[A any] interface { Error() error ValueE() (A, error) Value() A } +type BindFunc[A, B any] func(A) (B, error) + +type BindVoidFunc[A any] func(A) error + func Bind[A, B any](a Maybe[A], bindFunc BindFunc[A, B]) Maybe[B] { if a.Error() != nil { return NewFailure[B](a.Error()) @@ -18,56 +18,28 @@ func Bind[A, B any](a Maybe[A], bindFunc BindFunc[A, B]) Maybe[B] { return Result(bindFunc(a.Value())) } -type Success[A any] struct { - value A -} - -type Failure[A any] struct { - err error -} - -func (success *Success[A]) Error() error { - return nil -} - -func (success *Success[A]) ValueE() (A, error) { - return success.value, nil -} - -func (success *Success[A]) Value() A { - return success.value -} - -var ErrorNoValue = errors.New("cannot get value from failure Maybe") - -func (failure *Failure[A]) Error() error { - return failure.err -} +func BindVoid[A any](a Maybe[A], bindFunc BindVoidFunc[A]) Maybe[*interface{}] { + // Early escape if this is already a failed maybe + if a.Error() != nil { + return NewFailure[*interface{}](a.Error()) + } -func (failure *Failure[A]) ValueE() (A, error) { - return *new(A), ErrorNoValue -} + return VoidResult(bindFunc(a.Value())) -func (failure *Failure[A]) Value() A { - return *new(A) } -func NewSuccess[A any](value A) *Success[A] { - return &Success[A]{ - value: value, +func Result[A any](val A, err error) Maybe[A] { + if err != nil { + return NewFailure[A](err) } -} -func NewFailure[A any](err error) *Failure[A] { - return &Failure[A]{ - err: err, - } + return NewSuccess(val) } -func Result[A any](val A, err error) Maybe[A] { +func VoidResult(err error) Maybe[*interface{}] { if err != nil { - return NewFailure[A](err) + return NewFailure[*interface{}](err) } - return NewSuccess(val) + return NewSuccess[*interface{}](nil) } diff --git a/pkg/maybe/maybe_test.go b/pkg/maybe/maybe_test.go index dee32f3..10de161 100644 --- a/pkg/maybe/maybe_test.go +++ b/pkg/maybe/maybe_test.go @@ -41,7 +41,7 @@ func TestFailure(t *testing.T) { // If we use the accessor with an error, it should fail with an error returnedValue, returnedErr := failure.ValueE() - assert.Equal(t, maybe.ErrorNoValue, returnedErr) + assert.Equal(t, err, returnedErr) assert.Equal(t, "", returnedValue) } diff --git a/pkg/maybe/success.go b/pkg/maybe/success.go new file mode 100644 index 0000000..a9fce05 --- /dev/null +++ b/pkg/maybe/success.go @@ -0,0 +1,23 @@ +package maybe + +type Success[A any] struct { + value A +} + +func NewSuccess[A any](value A) *Success[A] { + return &Success[A]{ + value: value, + } +} + +func (success *Success[A]) Error() error { + return nil +} + +func (success *Success[A]) ValueE() (A, error) { + return success.value, nil +} + +func (success *Success[A]) Value() A { + return success.value +} diff --git a/pkg/report/default.go b/pkg/report/default.go index 686d883..fe167e3 100644 --- a/pkg/report/default.go +++ b/pkg/report/default.go @@ -21,8 +21,8 @@ func NewDefaultReport() framework.Report { Title: "Disk", Checks: []framework.Check{ &disk.SpaceCheck{}, - &disk.IopsCheck{}, - &disk.LatencyCheck{}, + disk.NewIopsCheck(), + disk.NewLatencyCheck(), }, }, {