Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/gossfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ command:
command produced no error output. Use `[]` only where you deliberately want
no assertion at all.

`goss validate` prints a warning to stderr for each empty list it finds. The
check is still skipped and still passes — the warning only tells you that the
line asserts nothing.

The `exec` attribute is the command to run; this defaults to the name of
the hash for backwards compatibility

Expand Down Expand Up @@ -300,6 +304,10 @@ file:
but it does not check that the file is empty. To assert emptiness, use
`contents: ""`.

`goss validate` warns on stderr about each one, so a gossfile generated by
`goss add file` produces one warning per file until you replace or remove
the `contents` line.

### gossfile

Import other gossfiles from this one. This is the best way to maintain a large number of tests, and/or create profiles.
Expand Down
4 changes: 2 additions & 2 deletions resource/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ func (c *Command) Validate(sys *system.System) []TestResult {

cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID()))
results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip))
if isSet(c.Stdout) {
if isSetWarnEmpty(c.Stdout, fmt.Sprintf("%s: command.stdout", c.ID())) {
results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
}
if isSet(c.Stderr) {
if isSetWarnEmpty(c.Stderr, fmt.Sprintf("%s: command.stderr", c.ID())) {
results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
}
return results
Expand Down
4 changes: 2 additions & 2 deletions resource/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ func (f *File) Validate(sys *system.System) []TestResult {
if f.Filetype != nil {
results = append(results, ValidateValue(f, "filetype", f.Filetype, sysFile.Filetype, skip))
}
if isSet(f.Contains) {
if isSetWarnEmpty(f.Contains, fmt.Sprintf("%s: file.contains", f.ID())) {
fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: file.contains has been renamed to file.contents\n")
results = append(results, ValidateValue(f, "contains", f.Contains, sysFile.Contents, skip))
}
if isSet(f.Contents) {
if isSetWarnEmpty(f.Contents, fmt.Sprintf("%s: file.contents", f.ID())) {
results = append(results, ValidateValue(f, "contents", f.Contents, sysFile.Contents, skip))
}
if f.Size != nil {
Expand Down
4 changes: 2 additions & 2 deletions resource/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ func (u *HTTP) Validate(sys *system.System) []TestResult {
if shouldSkip(results) {
skip = true
}
if isSet(u.Headers) {
if isSetWarnEmpty(u.Headers, fmt.Sprintf("%s: http.headers", u.ID())) {
results = append(results, ValidateValue(u, "Headers", u.Headers, sysHTTP.Headers, skip))
}
if isSet(u.Body) {
if isSetWarnEmpty(u.Body, fmt.Sprintf("%s: http.body", u.ID())) {
results = append(results, ValidateValue(u, "Body", u.Body, sysHTTP.Body, skip))
}

Expand Down
17 changes: 17 additions & 0 deletions resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,20 @@ func isSet(i interface{}) bool {
return i != nil
}
}

// isSetWarnEmpty reports whether a matcher property holds at least one
// condition, warning first when it holds a list written out as empty.
//
// A list of patterns is a list of conditions that must all hold, so an empty
// list is zero conditions: the property is skipped and always passes, which is
// rarely what someone writing it out by hand intends. The behaviour is
// deliberately left alone — goss add file emits contents: [] to record that it
// captured no expectation, and erroring would invalidate every gossfile it has
// ever generated. desc follows the same "<id>: <type>.<property>" shape as the
// deprecation warnings.
func isSetWarnEmpty(i any, desc string) bool {
if v, ok := i.([]any); ok && len(v) == 0 {
fmt.Fprintf(os.Stderr, "WARNING: %s is an empty list, which asserts nothing and always passes. Use \"\" to assert empty content, or remove it.\n", desc)
}
return isSet(i)
}
113 changes: 113 additions & 0 deletions resource/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package resource

import (
"bytes"
"io"
"os"
"strings"
"testing"
)

// captureStderr runs fn with os.Stderr redirected and returns what it wrote.
func captureStderr(t *testing.T, fn func()) string {
t.Helper()
orig := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer func() {
os.Stderr = orig
_ = w.Close()
_ = r.Close()
}()

os.Stderr = w
fn()
_ = w.Close()
os.Stderr = orig

var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatal(err)
}
return buf.String()
}

func TestIsSet(t *testing.T) {
cases := []struct {
name string
in any
want bool
}{
{"nil is unset", nil, false},
{"empty list is unset", []any{}, false},
{"list with one entry is set", []any{"foo"}, true},
{"empty string is set", "", true},
{"string is set", "foo", true},
{"false is set", false, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := isSet(c.in); got != c.want {
t.Errorf("isSet(%#v) = %v, want %v", c.in, got, c.want)
}
})
}
}

// TestIsSetWarnEmpty pins both halves of the contract: the warning fires only
// for a list written out as empty, and the set/unset answer is byte-for-byte
// what isSet already returned, so no matcher changes outcome.
func TestIsSetWarnEmpty(t *testing.T) {
cases := []struct {
name string
in any
wantWarn bool
}{
{"empty list warns", []any{}, true},
{"list with one entry does not warn", []any{"foo"}, false},
{"nil does not warn", nil, false},
{"empty string does not warn", "", false},
{"string does not warn", "foo", false},
{"empty map does not warn", map[string]any{}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var got bool
out := captureStderr(t, func() {
got = isSetWarnEmpty(c.in, "/some/id: file.contents")
})

if want := isSet(c.in); got != want {
t.Errorf("isSetWarnEmpty(%#v) = %v, want isSet's %v", c.in, got, want)
}

warned := strings.Contains(out, "WARNING:")
if warned != c.wantWarn {
t.Errorf("isSetWarnEmpty(%#v) warned = %v, want %v (output %q)", c.in, warned, c.wantWarn, out)
}
})
}
}

func TestIsSetWarnEmptyNamesTheProperty(t *testing.T) {
out := captureStderr(t, func() {
isSetWarnEmpty([]any{}, "sh -c 'echo boom >&2': command.stderr")
})

for _, want := range []string{
"WARNING:",
"sh -c 'echo boom >&2': command.stderr",
"empty list",
"always passes",
`use "" to assert empty content`,
} {
if !strings.Contains(strings.ToLower(out), strings.ToLower(want)) {
t.Errorf("warning %q is missing %q", out, want)
}
}
if n := strings.Count(out, "\n"); n != 1 {
t.Errorf("expected exactly one warning line, got %d in %q", n, out)
}
}