diff --git a/nagios.go b/nagios.go index 06f58e4..fe4d6fa 100644 --- a/nagios.go +++ b/nagios.go @@ -579,7 +579,8 @@ func (p *Plugin) SkipOSExit() { // SetPayloadBytes uses the given input in bytes to overwrite any existing // content in the payload buffer. It returns the length of input and a -// potential error. +// potential error. If given empty input the payload buffer is reset without +// adding any content. // // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. @@ -591,12 +592,17 @@ func (p *Plugin) SetPayloadBytes(input []byte) (int, error) { p.encodedPayloadBuffer.Reset() + if len(input) == 0 { + return 0, nil + } + return p.encodedPayloadBuffer.Write(input) } // SetPayloadString uses the given input string to overwrite any existing // content in the payload buffer. It returns the length of input and a -// potential error. +// potential error. If given empty input the payload buffer is reset without +// adding any content. // // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. @@ -608,15 +614,24 @@ func (p *Plugin) SetPayloadString(input string) (int, error) { p.encodedPayloadBuffer.Reset() + if len(input) == 0 { + return 0, nil + } + return p.encodedPayloadBuffer.WriteString(input) } // AddPayloadBytes appends the given input in bytes to the payload buffer. It -// returns the length of input and a potential error. +// returns the length of input and a potential error. Empty input is silently +// ignored. // // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) AddPayloadBytes(input []byte) (int, error) { + if len(input) == 0 { + return 0, nil + } + p.logAction(fmt.Sprintf( "Appending %d bytes input to payload buffer", len(input), @@ -626,11 +641,16 @@ func (p *Plugin) AddPayloadBytes(input []byte) (int, error) { } // AddPayloadString appends the given input string to the payload buffer. It -// returns the length of input and a potential error. +// returns the length of input and a potential error. Empty input is silently +// ignored. // // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) AddPayloadString(input string) (int, error) { + if len(input) == 0 { + return 0, nil + } + p.logAction(fmt.Sprintf( "Appending %d bytes input to payload buffer", len(input), @@ -640,7 +660,8 @@ func (p *Plugin) AddPayloadString(input string) (int, error) { } // UnencodedPayload returns the payload buffer contents in string format as-is -// without encoding applied. +// without encoding applied. If the payload buffer is empty an empty string is +// returned. func (p *Plugin) UnencodedPayload() string { p.logAction(fmt.Sprintf( "Returning %d bytes from payload buffer", diff --git a/unexported_test.go b/unexported_test.go index 8d7a65e..7433e72 100644 --- a/unexported_test.go +++ b/unexported_test.go @@ -428,6 +428,72 @@ func TestSetPayloadString_SetsInputSuccessfullyWhenCalledMultipleTimes(t *testin } } +// TestSetPayloadString_SetsInputSuccessfullyWhenCalledWithEmptyInput asserts +// that when given input the `SetPayloadString` method (with overwrite +// behavior) resets the payload buffer without returning an error or producing +// any output. +func TestSetPayloadString_SetsInputSuccessfullyWhenCalledWithEmptyInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + tests := map[string]struct { + // This represents data as given before it is written to the + // payload buffer (unencoded). + input string + }{ + "empty string": { + input: "", + }, + } + + for name, tt := range tests { + // Guard against referencing the loop iterator variable directly. + // + // https://stackoverflow.com/questions/68559574/using-the-variable-on-range-scope-x-in-function-literal-scopelint + // https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tt := tt + + t.Run(name, func(t *testing.T) { + t.Logf("Evaluating input %q", tt.input) + + // We skip explicitly resetting the buffer as we do not expect it + // to change. + // + // plugin.encodedPayloadBuffer.Reset() + + repeat := 2 + for i := 0; i < repeat+1; i++ { + written, err := plugin.SetPayloadString(tt.input) + + if err != nil { + t.Fatalf("Error occurred processing given empty input: %v", err) + } + + if written > 0 { + t.Fatalf("Unexpected write of %d bytes to payload buffer", written) + } else { + t.Logf("Successfully handled %d bytes given input", written) + } + } + + // Repeat function call (with overwrite behavior) should normally + // produce non-repeating output. In our case we processed empty + // input; the buffer should remain empty no matter how many times + // we provided empty input. + want := tt.input + + got := plugin.encodedPayloadBuffer.String() + + if d := cmp.Diff(want, got); d != "" { + t.Errorf("(-want, +got)\n:%s", d) + } else { + t.Logf("OK: Payload buffer matches given empty input.") + } + }) + } +} + // TestSetPayloadBytes_SetsInputSuccessfullyWhenCalledOnce asserts that a // payload buffer populated via the `SetPayloadBytes` method (overwrite // behavior) with non-repeating valid input produces valid output. @@ -541,6 +607,80 @@ func TestSetPayloadBytes_SetsInputSuccessfullyWhenCalledMultipleTimes(t *testing } } +// TestSetPayloadBytes_SetsInputSuccessfullyWhenCalledWithEmptyInput asserts +// that when given input the `SetPayloadBytes` method (with overwrite +// behavior) resets the payload buffer without returning an error or producing +// any output. +func TestSetPayloadBytes_SetsInputSuccessfullyWhenCalledWithEmptyInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + tests := map[string]struct { + // This represents data as given before it is written to the + // payload buffer (unencoded). + input []byte + }{ + "empty string": { + input: []byte(""), + }, + "empty byte slice": { + input: []byte{}, + }, + "nil": { + input: nil, + }, + } + + for name, tt := range tests { + // Guard against referencing the loop iterator variable directly. + // + // https://stackoverflow.com/questions/68559574/using-the-variable-on-range-scope-x-in-function-literal-scopelint + // https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tt := tt + + t.Run(name, func(t *testing.T) { + t.Logf("Evaluating input %#v", tt.input) + + // We skip explicitly resetting the buffer as we do not expect it + // to change. + // + // plugin.encodedPayloadBuffer.Reset() + + repeat := 2 + for i := 0; i < repeat+1; i++ { + written, err := plugin.SetPayloadBytes(tt.input) + + if err != nil { + t.Fatalf("Error occurred processing given empty input: %v", err) + } + + if written > 0 { + t.Fatalf("Unexpected write of %d bytes to payload buffer", written) + } else { + t.Logf("Successfully handled %d bytes given input", written) + } + } + + // Repeat function call (with overwrite behavior) should normally + // produce non-repeating output. In our case we processed empty + // input; the buffer should remain empty no matter how many times + // we provided empty input. + want := tt.input + got := plugin.encodedPayloadBuffer.Bytes() + + // We compare by length to handle comparison of empty byte slice + // to nil. + if len(want) != len(got) { + d := cmp.Diff(want, got) + t.Errorf("(-want, +got)\n:%s", d) + } else { + t.Logf("OK: Payload buffer length reflects given empty input.") + } + }) + } +} + // TestAddPayloadString_AppendsInputSuccessfullyWhenCalledOnce asserts that a // payload buffer populated via the `AddPayloadString` method with // non-repeating valid input produces valid output. @@ -656,6 +796,66 @@ func TestAddPayloadString_AppendsInputSuccessfullyWhenCalledMultipleTimes(t *tes } } +// TestAddPayloadString_AppendsNothingWhenCalledWithEmptyInput asserts that +// when given empty input the `AddPayloadString` method does not return an +// error and produces no output. +func TestAddPayloadString_AppendsNothingWhenCalledWithEmptyInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + tests := map[string]struct { + input string + }{ + "empty string": { + input: "", + }, + } + + for name, tt := range tests { + // Guard against referencing the loop iterator variable directly. + // + // https://stackoverflow.com/questions/68559574/using-the-variable-on-range-scope-x-in-function-literal-scopelint + // https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tt := tt + + t.Run(name, func(t *testing.T) { + t.Logf("Evaluating input %#v", tt.input) + + plugin.encodedPayloadBuffer.Reset() + + repeat := 2 + for i := 0; i < repeat+1; i++ { + written, err := plugin.AddPayloadString(tt.input) + + if err != nil { + t.Fatalf("Error occurred appending given empty input to payload buffer: %v", err) + } + + if written > 0 { + t.Fatalf("Unexpected append of %d bytes to payload buffer", written) + } else { + t.Logf("Successfully handled %d bytes given input", written) + } + } + + // Repeat function call should produce no collected output. + var want string + for i := 0; i < repeat+1; i++ { + want += tt.input + } + + got := plugin.encodedPayloadBuffer.String() + + if d := cmp.Diff(want, got); d != "" { + t.Errorf("(-want, +got)\n:%s", d) + } else { + t.Logf("OK: Payload buffer matches total given input.") + } + }) + } +} + // TestAddPayloadBytes_AppendsInputSuccessfullyWhenCalledOnce asserts that a // payload buffer populated via the `AddPayloadBytes` method with // non-repeating valid input produces valid output. @@ -771,6 +971,72 @@ func TestAddPayloadBytes_AppendsInputSuccessfullyWhenCalledMultipleTimes(t *test } } +// TestAddPayloadBytes_AppendsNothingWhenCalledWithEmptyInput asserts that +// when given empty input the `AddPayloadBytes` method does not return an +// error and produces no output. +func TestAddPayloadBytes_AppendsNothingWhenCalledWithEmptyInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + tests := map[string]struct { + input []byte + }{ + "empty string": { + input: []byte(""), + }, + "empty byte slice": { + input: []byte{}, + }, + "nil": { + input: nil, + }, + } + + for name, tt := range tests { + // Guard against referencing the loop iterator variable directly. + // + // https://stackoverflow.com/questions/68559574/using-the-variable-on-range-scope-x-in-function-literal-scopelint + // https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tt := tt + + t.Run(name, func(t *testing.T) { + t.Logf("Evaluating input %#v", tt.input) + + plugin.encodedPayloadBuffer.Reset() + + repeat := 2 + for i := 0; i < repeat+1; i++ { + written, err := plugin.AddPayloadBytes(tt.input) + + if err != nil { + t.Fatalf("Error occurred appending given empty input to payload buffer: %v", err) + } + + if written > 0 { + t.Fatalf("Unexpected append of %d bytes to payload buffer", written) + } else { + t.Logf("Successfully handled %d bytes given input", written) + } + } + + // Repeat function call should produce no collected output. + var want string + for i := 0; i < repeat+1; i++ { + want += string(tt.input) + } + + got := plugin.encodedPayloadBuffer.String() + + if d := cmp.Diff(want, got); d != "" { + t.Errorf("(-want, +got)\n:%s", d) + } else { + t.Logf("OK: Payload buffer matches total given input.") + } + }) + } +} + // TestEmptyPerfDataAndEmptyServiceOutputProducesNoOutput asserts that an // empty Performance Data metrics collection AND empty ServiceOutput produces // no output.