Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 interface{}, desc string) bool {
if v, ok := i.([]interface{}); ok && len(v) == 0 {
Comment thread
kgaughan marked this conversation as resolved.
Outdated
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)
}
}