diff --git a/.travis.yml b/.travis.yml
index be5e022c5..6409b67c2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -41,8 +41,8 @@ deploy:
api_key:
secure: ijNltjw/mIHIOx8vLZ6asUun3SbY7D+XZbs5NX8vcIv0jvOiwaaT1hqny7SQBHfGZzqHsYUSS/GYAYJdBqKFFfGmTZsl90hFT6D0RGdz9C71UVxNFX4wQ5KQ/WVvdMT2SrLymGvu9TvoU0VG8OWqWVdxSlUPf6qOTGAagrzg+Tbsbb6czeiG67mlBBL23XSlfMG1p45UxzvI41SZj2R3ElUb0hym1CrFaoC36PBGrb0x41TXzvd8J7cu6xDzgczYhnYQQZpS6f2YcqNV1z0f+P67EQqQiDWIIcK2jE/YG+RgM8cbpLMiMec8CDiwNCsejBA5EbVMlGJlODvBXT5NmMBeugueqfSHEfkl5qZTQG4AOAT7UsqbnM7r0NqzmaE5Lj90igvJK6rNsH1ZRe79WfSsTtuzlkkouHGvyoz0M8gnMSzpbbwoyIy+UT0hhPMoZvIpXfr43en5WkbkPKfop0p4Vjc8NGg0iD45q1JAvIVTtz/WvWTknM1P8e3u+TiDTaZkcJJmFaBqgaeLoWktOGfi54p9nhgQnSyBYt4PyvhWDQs7QFmX0BdKlqJCESvUOJTe1t6zJJsV7Gn/3sGCN7JUEwbnXTsCoMjjFFUvQdm0Ur7t7/2xU3kO+dyfqcdM/5SYFeppQcjHI0ckhI51mIoBTsJsGvaVwKKL1I4cyBU=
file:
- - release/goss-alpha-darwin-amd64
- - release/goss-alpha-darwin-amd64.sha256
+ - release/goss-darwin-amd64
+ - release/goss-darwin-amd64.sha256
- release/goss-linux-amd64
- release/goss-linux-amd64.sha256
- release/goss-linux-386
diff --git a/Makefile b/Makefile
index 01fa73b94..5ddfb3d5e 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,20 @@ install: release/goss-linux-amd64
test:
$(info INFO: Starting build $@)
- ./ci/go-test.sh $(pkgs)
+ ./ci/go-test.sh
+
+cov:
+ go test -coverpkg=./... -coverprofile=c.out ./...
+ # go tool cover -func ./c.out
+
+funcov:
+ go test -coverpkg=./... -coverprofile=c.out ./...
+ go tool cover -func ./c.out
+
+htmlcov:
+ go test -v -coverpkg=./... -coverprofile=c.out ./...
+ go tool cover -html ./c.out
+
lint:
$(info INFO: Starting build $@)
@@ -36,15 +49,13 @@ bench:
$(info INFO: Starting build $@)
go test -bench=.
-alpha-test-%: release/goss-%
+test-int-validate-%: release/goss-%
$(info INFO: Starting build $@)
- ./integration-tests/run-tests-alpha.sh $*
+ ./integration-tests/run-validate-tests.sh $*
test-int-serve-%: release/goss-%
$(info INFO: Starting build $@)
./integration-tests/run-serve-tests.sh $*
-# shim to account for linux being not in alpha
-test-int-serve-linux-amd64: test-int-serve-alpha-linux-amd64
release/goss-%: $(GO_FILES)
./release-build.sh $*
@@ -71,6 +82,10 @@ push-images:
$(info INFO: Starting build $@)
development/push_images.sh
+# Update the matcher test golden files
+update-matcher-tests:
+ go test -v -run '^TestMatchers' . -update
+
test-darwin-all: test-short-all test-int-darwin-all
# linux _does_ have the docker-style testing, but does _not_ currently have the same style integration tests darwin+windows do, _because_ of the docker-style testing.
test-linux-all: test-short-all test-int-64 test-int-32
@@ -78,8 +93,8 @@ test-windows-all: test-short-all test-int-windows-all
test-int-64: centos7 wheezy trusty alpine3 arch test-int-serve-linux-amd64
test-int-32: centos7-32 wheezy-32 trusty-32 alpine3-32 arch-32
-test-int-darwin-all: alpha-test-alpha-darwin-amd64 test-int-serve-alpha-darwin-amd64
-test-int-windows-all: alpha-test-alpha-windows-amd64 test-int-serve-alpha-windows-amd64
+test-int-darwin-all: test-int-validate-darwin-amd64 test-int-serve-darwin-amd64
+test-int-windows-all: test-int-validate-windows-amd64 test-int-serve-windows-amd64
test-int-all: test-int-32 test-int-64
centos7-32: build
diff --git a/README.md b/README.md
index 23822927e..dc03e7508 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,6 @@
-**Note:** For an even faster way of doing this, see: [autoadd](https://github.com/goss-org/goss/blob/master/docs/manual.md#autoadd-aa---auto-add-all-matching-resources-to-test-suite)
-
**Note:** For testing docker containers see the [dgoss](https://github.com/goss-org/goss/tree/master/extras/dgoss) wrapper. Also, user submitted wrapper scripts for Kubernetes [kgoss](https://github.com/goss-org/goss/tree/master/extras/kgoss) and Docker Compose [dcgoss](https://github.com/goss-org/goss/tree/master/extras/dcgoss).
**Note:** For some Docker/Kubernetes healthcheck, health endpoint, and
diff --git a/ci/go-test.sh b/ci/go-test.sh
index e715f991e..d0623f91f 100755
--- a/ci/go-test.sh
+++ b/ci/go-test.sh
@@ -3,7 +3,7 @@ set -euo pipefail
command -v go
-go test -coverprofile="c.out" "${1}"
+go test -coverpkg=./... ./... -skip '^TestPrometheus' -coverprofile="c.out"
sed 's|github.com/goss-org/goss/||' <"c.out" >"c.out.tmp"
diff --git a/cmd/goss/goss.go b/cmd/goss/goss.go
index f720fc76e..203ac1e8d 100644
--- a/cmd/goss/goss.go
+++ b/cmd/goss/goss.go
@@ -67,7 +67,6 @@ func timeoutFlag(value time.Duration) cli.DurationFlag {
}
func main() {
- startTime := time.Now()
app := cli.NewApp()
app.EnableBashCompletion = true
app.Version = version
@@ -149,7 +148,7 @@ func main() {
},
Action: func(c *cli.Context) error {
fatalAlphaIfNeeded(c)
- code, err := goss.Validate(newRuntimeConfigFromCLI(c), startTime)
+ code, err := goss.Validate(newRuntimeConfigFromCLI(c))
if err != nil {
color.Red(fmt.Sprintf("Error: %v\n", err))
}
@@ -416,14 +415,14 @@ func addAlphaFlagIfNeeded(app *cli.App) {
if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
app.Flags = append(app.Flags, cli.StringFlag{
Name: "use-alpha",
- Usage: fmt.Sprintf("goss is alpha-quality. Set to 1 to use anyway."),
+ Usage: fmt.Sprintf("goss on macOS/Windows is alpha-quality. Set to 1 to use anyway."),
EnvVar: "GOSS_USE_ALPHA",
Value: "0",
})
}
}
-const msgFormat string = `WARNING: goss for this platform (%q) is alpha-quality, work-in-progress, and not yet exercised within continuous integration.
+const msgFormat string = `WARNING: goss for this platform (%q) is alpha-quality, work-in-progress and community-supported.
You should not expect everything to work. Treat linux as the canonical behaviour to expect.
diff --git a/docs/manual.md b/docs/manual.md
index c339a8f8e..8b201c635 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -4,59 +4,6 @@
## Table of Contents
-- [goss manual](#goss-manual)
- - [Table of Contents](#table-of-contents)
- - [Usage](#usage)
- - [global options](#global-options)
- - [--gossfile gossfile | -g gossfile](#--gossfile-gossfile-or--g-gossfile)
- - [--vars](#--vars)
- - [--package ](#--package-type)
- - [commands](#commands)
- - [add, a - Add system resource to test suite](#add-a---add-system-resource-to-test-suite)
- - [Resource types](#resource-types)
- - [Flags](#flags)
- - [--exclude-attr](#--exclude-attr)
- - [Example:](#example)
- - [autoadd, aa - Auto add all matching resources to test suite](#autoadd-aa---auto-add-all-matching-resources-to-test-suite)
- - [Example:](#example-1)
- - [render, r - Render gossfile after importing all referenced gossfiles](#render-r---render-gossfile-after-importing-all-referenced-gossfiles)
- - [Flags](#flags-1)
- - [--debug](#--debug)
- - [Example:](#example-2)
- - [serve, s - Serve a health endpoint](#serve-s---serve-a-health-endpoint)
- - [Flags](#flags-2)
- - [Example:](#example-3)
- - [validate, v - Validate the system](#validate-v---validate-the-system)
- - [Flags](#flags-3)
- - [Examples:](#examples)
- - [Goss test creation](#goss-test-creation)
- - [Important note about goss file format](#important-note-about-goss-file-format)
- - [Available tests](#available-tests)
- - [addr](#addr)
- - [command](#command)
- - [dns](#dns)
- - [file](#file)
- - [gossfile](#gossfile)
- - [group](#group)
- - [http](#http)
- - [interface](#interface)
- - [kernel-param](#kernel-param)
- - [mount](#mount)
- - [matching](#matching)
- - [With Templates:](#with-templates)
- - [Without Templates:](#without-templates)
- - [package](#package)
- - [port](#port)
- - [process](#process)
- - [service](#service)
- - [user](#user)
- - [Patterns](#patterns)
- - [Example](#example-4)
- - [Advanced Matchers](#advanced-matchers)
- - [Examples](#examples-1)
- - [Templates](#templates)
- - [Examples](#examples-2)
-
## Usage
```
@@ -321,8 +268,9 @@ The `application/vnd.goss-{output format}` media type can be used in the `Accept
* `silent` - No output. Avoids exposing system information (e.g. when serving tests as a healthcheck endpoint)
* `--format-options`, `-o` (output format option)
* `perfdata` - Outputs Nagios "performance data". Applies to `nagios` output
- * `verbose` - Gives verbose output. Applies to `nagios` output
- * `pretty` - Pretty printing for the `json` output
+ * `verbose` - Gives verbose output. Applies to `nagios` output
+ * `pretty` - Pretty printing for the `json` output
+ * `sort` - Sorts the results
* `--loglevel level`, `-L level` - Goss logging verbosity level (default: `INFO`). `level` can be one of `TRACE | DEBUG | INFO | WARN | ERROR | FATAL`. Lower levels of tracing include all upper levels traces also (ie. INFO include WARN, ERROR and FATAL outputs).
* `TRACE` - Print details for each check, successful or not and all incoming healthchecks
* `DEBUG` - Print details of summary response to healthchecks including remote IP address, return code and full body
@@ -488,24 +436,27 @@ Validates if a remote `address:port` are accessible.
```yaml
addr:
tcp://ip-address-or-domain-name:80:
+ # required attributes
reachable: true
- timeout: 500
# optional attributes
+ # defaults to hash key
+ address: "tcp://ip-address-or-domain-name:80"
+ timeout: 500
local-address: 127.0.0.1
```
### command
-Validates the exit-status and output of a command
+Validates the exit-status and output of a command. This can be used in combination with the [gjson](#gjson) matcher to create powerful goss custom tests.
```yaml
command:
- version:
+ 'go version':
# required attributes
exit-status: 0
+ # optional attributes
# defaults to hash key
exec: "go version"
- # optional attributes
stdout:
- go version go1.6 linux/amd64
stderr: []
@@ -527,6 +478,8 @@ dns:
# required attributes
resolvable: true
# optional attributes
+ # defaults to hash key
+ resolve: localhost
addrs:
- 127.0.0.1
- ::1
@@ -593,12 +546,14 @@ file:
# required attributes
exists: true
# optional attributes
+ # defaults to hash key
+ path: /etc/passwd
mode: "0644"
size: 2118 # in bytes
owner: root
group: root
filetype: file # file, symlink, directory
- contains: [] # Check file content for these patterns
+ contents: [] # Check file content for these patterns
md5: 7c9bb14b3bf178e82c00c2a4398c93cd # md5 checksum of file
# A stronger checksum alternatives to md5 (recommended)
sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c
@@ -612,7 +567,7 @@ file:
skip: false
```
-`contains` can be a string or a [pattern](#patterns)
+`contents` can be a string or a [pattern](#patterns)
### gossfile
@@ -642,6 +597,8 @@ group:
# required attributes
exists: true
# optional attributes
+ # defaults to hash key
+ groupname: /etc/passwd
gid: 65534
skip: false
```
@@ -656,6 +613,8 @@ http:
# required attributes
status: 200
# optional attributes
+ # defaults to hash key
+ url: https://www.google.com
allow-insecure: false
no-follow-redirects: false # Setting this to true will NOT follow redirects
timeout: 1000
@@ -685,6 +644,8 @@ interface:
# required attributes
exists: true
# optional attributes
+ # defaults to hash key
+ name: eth0
addrs:
- 172.17.0.2/16
- fe80::42:acff:fe11:2/64
@@ -700,6 +661,9 @@ kernel-param:
kernel.ostype:
# required attributes
value: Linux
+ # optional attributes
+ # defaults to hash key
+ name: kernel.ostype
```
To see the full list of current values, run `sysctl -a`.
@@ -714,6 +678,8 @@ mount:
# required attributes
exists: true
# optional attributes
+ # defaults to hash key
+ mountpoint: /home
opts:
- rw
- relatime
@@ -776,8 +742,6 @@ matching:
baz: bing
matches:
and:
- - have-key-with-value:
- foo: bar
- have-key: baz
```
@@ -790,6 +754,8 @@ package:
# required attributes
installed: true
# optional attributes
+ # defaults to hash key
+ name: httpd
versions:
- 2.2.15
skip: false
@@ -810,6 +776,8 @@ port:
# required attributes
listening: true
# optional attributes
+ # defaults to hash key
+ port: 'tcp:22'
ip: # what IP(s) is it listening on
- 0.0.0.0
skip: false
@@ -824,10 +792,13 @@ process:
chrome:
# required attributes
running: true
+ # optional attributes
+ # defaults to hash key
+ comm: chrome
skip: false
```
-**NOTE:** This check is inspecting the name of the binary, not the name of the process. For example, a process with the name `nginx: master process /usr/sbin/nginx` would be checked with the process `nginx`. To discover the binary of a pid run `ps -p -o comm`.
+**NOTE:** This check is inspecting the name of the binary, not the name of the process. For example, a process with the name `nginx: master process /usr/sbin/nginx` would be checked with the process `nginx`. To discover the binary of a pid run `cat -E /proc//comm`.
### service
Validates the state of a service.
@@ -835,12 +806,17 @@ Validates the state of a service.
```yaml
service:
sshd:
- # required attributes
+ # Optional attributes
+ # defaults to hash key
+ name: sshd
enabled: true
running: true
+ runlevels: ["3", "4", "5"] # Alpine example, runlevels: ["default"]
skip: false
```
+`runlevels` is only supported on Alpine init, sysv init, and upstart
+
**NOTE:** this will **not** automatically check if the process is alive, it will check the status from `systemd`/`upstart`/`init`.
@@ -853,6 +829,8 @@ user:
# required attributes
exists: true
# optional attributes
+ # defaults to hash key
+ username: nfsnobody
uid: 65534
gid: 65534
groups:
@@ -865,91 +843,293 @@ user:
**NOTE:** This check is inspecting the contents of local passwd file `/etc/passwd`, this does not validate remote users (e.g. LDAP).
-## Patterns
-For the attributes that use patterns (ex. `file`, `command` `output`), each pattern is checked against the attribute string, the type of patterns are:
+## Matchers
+
+### Default Matchers
+
+Default matchers are determined by the attribute value received from the system.
+
+#### Bool, Strings, Integers
+
+Bool, Strings and integers are compared using equality, for example:
+
+```yaml
+matching:
+ basic_string:
+ content: 'foo'
+ matches: 'foo'
+
+user:
+ nfsnobody:
+ exists: true
+ uid: 65534
+```
+
+#### Arrays
+
+Arrays are treated as a [contains-elements](#array-matchers) by default, this validates that the expected test is a subset of the returned system state.
+
+```yaml
+matching:
+ basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ - 'group1'
+ - 'group2'
+
+ # This fails, since the returned result and it's no longer a subset
+ basic_array_failing:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ - 'group1'
+ - 'group2'
+ - 'group2' # this 2nd group2 is not in the returned content
+```
+
+#### io.Readers
-* `"string"` - checks if any line contain string.
-* `"!string"` - inverse of above, checks that no line contains string
-* `"\\!string"` - escape sequence, check if any line contains `"!string"`
-* `"/regex/"` - verifies that line contains regex
-* `"!/regex/"` - inverse of above, checks that no line contains regex
+This is the most magical matcher for goss. It remains a default for historic and performance reasons. Some attributes return an io.Reader that is read line by line (ex. file content, command, http body). This allows goss to validate large files/content efficiently.
-**NOTE:** Pattern attributes do not support [Advanced Matchers](#advanced-matchers)
-**NOTE:** Regex support is based on golang's regex engine documented [here](https://golang.org/pkg/regexp/syntax/)
+Each pattern is checked against the attribute output, the type of patterns are:
+
+* `"foo"` - checks if any line contains `foo`
+* `"!foo"` - inverse of above, checks that no line contains `foo`
+* `"\\!foo"` - escape sequence, check if any line contains `!string`
+* `"/[Rr]egex/"` - verifies that line matches regex
+* `"!/[Rr]egex/"` - inverse of above, checks that no line matches regex
+
+**NOTE:** Regex support is based on Golang's regex engine documented [here](https://golang.org/pkg/regexp/syntax/)
**NOTE:** You will **need** the double backslash (`\\`) escape for Regex special entities, for example `\\s` for blank spaces.
-### Example
-```bash
-$ cat /tmp/test.txt
-found
-!alsofound
+Example:
+
+```yaml
+file:
+ /tmp/test.txt:
+ exists: true
+ contents:
+ - "foo"
+ - "!bar"
+ - "/[Gg]oss/"
+```
+The above can be expressed as:
-$ cat goss.yaml
+```yaml
file:
/tmp/test.txt:
exists: true
- contains:
- - found
- - /fou.d/
- - "\\!alsofound"
- - "!missing"
- - "!/mis.ing/"
+ contents:
+ and:
+ - contain-element: "foo"
+ - not: {contain-element: "bar"}
+ - contain-element: {match-regexp: "[Gg]oss"}
+
+```
+
+### Transforms
+
+If the system state type and the expected type don't match, goss will attempt to transform the system state type before matching it.
-$ goss validate
-..
+For example, kernel-param attribute returns a string, however, it can be tested using numeric comparisons:
+
+Example kernel-param test:
+```yaml
+kernel-param:
+ net.core.somaxconn:
+ value: "128"
+```
+
+Example (failing) kernel-param test with transform:
+```yaml
+kernel-param:
+ net.core.somaxconn:
+ value: {gt: 200}
+```
+
+When a transformed test fails, it will detail the transformers used, the `-o include_raw` option can be used to include the raw, untransformed attribute value:
+```
+$ goss v
+F
+
+Failures/Skipped:
+
+KernelParam: net.core.somaxconn: value:
+Expected
+ 128
+to be >
+ 200
+the transform chain was
+ [{"to-numeric":{}}]
+
+Total Duration: 0.001s
+Count: 1, Failed: 1, Skipped: 0
+
+
+$ goss v -o include_raw
+F
+
+Failures/Skipped:
+
+KernelParam: net.core.somaxconn: value:
+Expected
+ 128
+to be >
+ 200
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128"
Total Duration: 0.001s
-Count: 2, Failed: 0
+Count: 1, Failed: 1, Skipped: 0
+
```
-## Advanced Matchers
-Goss supports advanced matchers by converting json input to [gomega](https://onsi.github.io/gomega/) matchers.
+### Advanced Matchers
-### Examples
+Goss supports advanced matchers by converting YAML input to [gomega](https://onsi.github.io/gomega/) matchers.
-Validate that user `nobody` has a `uid` that is less than `500` and that they are **only** a member of the `nobody` group.
+#### String Matchers
+These will convert the system attribute to a string prior to matching.
+
+* `'55'` - Checks that the numeric is "55" when converted to string
+* `have-prefix: pre` - Checks if string starts with "pre"
+* `have-suffix: suf` - Checks if string ends with "suf"
+* `match-regexp: '.*'` - Checks if string matches regexp
+* `contain-substring: '2'` - Checks if string contains "2"
+
+Example:
```yaml
-user:
- nobody:
- exists: true
- uid:
- lt: 500
- groups:
- consist-of: [nobody]
+matching:
+ example:
+ content: 42
+ matches:
+ and:
+ - '42'
+ - have-prefix: '4'
+ - have-suffix: '2'
+ - match-regexp: '\d{2}'
+ - contain-substring: '2'
```
-Matchers can be nested for more complex logic, for example you can ensure that you have 3 kernel versions installed and none of them are `4.1.0`:
+#### Numeric matchers
+These will convert the system attribute to a numeric prior to matching.
+
+* `42` - If the expected type is a number
+* `gt, ge, lt, le` - Greater than, greater than or equal, less than, etc..
+
+Example:
```yaml
-package:
- kernel:
- installed: true
- versions:
+matching:
+ example:
+ content: "42"
+ matches:
and:
- - have-len: 3
- - not:
- contain-element: "4.1.0"
+ - 42
+ - 42.0
+ - gt: 40
+ - lt: 45
```
-Custom semver matcher is available under `semver-constraint`:
+#### Array matchers
+
+These will convert the system attribute to an array prior to matching. Strings are split on "\n"
+
+* `contain-element: matcher` - Checks if the array contains an element that passes the matcher
+* `contain-elements: [matcher, ...]` - checks if the array is a superset of the provided matchers
+* `[matcher, ...]` - same as above
+* `equal: [value, ...]` - Checks if the array is exactly equal to provided array
+* `consist-of: [matcher, ...]` - Checks if the array consists of the provided matchers (order does not matter)
+
+Example:
```yaml
-example:
- content:
- - 1.0.1
- - 1.9.9
- matches:
- semver-constraint: ">1.0.0 <2.0.0 !=1.5.0"
+matching:
+ example:
+ content: [foo, bar, moo]
+ matches:
+ and:
+ - contain-elements: [foo, bar]
+ - [foo, bar] # same as above
+ - equal: [foo, bar, moo] # order matters, exact match
+ - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
+ - contain-element:
+ have-prefix: b
```
-For more information see:
-* [gomega_test.go](https://github.com/goss-org/goss/blob/master/resource/gomega_test.go) - For a complete set of supported json -> Gomega mapping
-* [gomega](https://onsi.github.io/gomega/) - Gomega matchers reference
-* [semver](https://github.com/blang/semver#ranges) - Semver constraint (or range) syntax
+#### Misc matchers
+
+These matchers don't really fall into any of the above categories, or span multiple categories.
+
+* `equal` - Useful when needing to override a default matcher
+* `have-len: 3` - Checks if the array/string/map has length of 3
+* `have-key: "foo"` - Checks if key exists in map, useful with `gjson`
+* `not: matcher` - Checks that a matcher does not match
+* `and: [matcher, ..]` - Checks that all matchers match
+* `or: [matcher, ..]` - Checks that any matchers match
+ * when system returns a string it is converted into a one element array and matched
+
+See the following for examples: [link..]fixme
+
+##### semver-constraint
+
+Checks that all versions match semver constraint or range syntax. This uses [semver](https://github.com/blang/semver) under the hood, however, wildcards (e.g. `1.X` are not officially supported and may go away in a future release.
+
+Example:
+```yaml
+matching:
+ semver:
+ content:
+ - 1.0.1
+ - 1.9.9
+ matches:
+ semver-constraint: ">1.0.0 <2.0.0 !=1.5.0"
+ semver2:
+ content:
+ - 1.0.1
+ - 1.5.0
+ - 1.9.9
+ matches:
+ not:
+ semver-constraint: ">1.0.0 <2.0.0 !=1.5.0"
+ semver3:
+ content: 1.0.1
+ matches:
+ semver-constraint: ">5.0.0 || < 1.5.0"
+```
+
+
+##### gjson
+
+Checks extracted [gjson](https://gjson.dev/) passes the matcher
+
+Example:
+```yaml
+matching:
+ example:
+ content: '{"foo": "bar", "moo" {"nested": "cow"}, "count": "15"}'
+ matches:
+ gjson:
+ moo.nested: cow
+ foo: {have-prefix: b}
+ count: {le: 25}
+ '@this': {have-key: "foo"}
+ moo:
+ and:
+ - {have-key: "nested"}
+ - {not: {have-key: "nested2"}}
+```
## Templates
diff --git a/docs/platform-feature-parity.md b/docs/platform-feature-parity.md
index cba5e3a17..c5f388503 100644
--- a/docs/platform-feature-parity.md
+++ b/docs/platform-feature-parity.md
@@ -4,7 +4,8 @@ macOS and Windows binaries are new and considered alpha-quality. Some functional
To clearly signal that, goss emits a log message on every invocation saying so, linking here, then exits with a clear error.
-To try out the alpha functionality, you must do one of
+To try out the alpha functionality, you must do one of:
+
* pass `--use-alpha=1` to the root command - e.g. `goss --use-alpha=1 validate`.
* set an environment variable `GOSS_USE_ALPHA=1`.
@@ -140,8 +141,8 @@ You can find goss-files that are used to populate this matrix within `integratio
Run all of the `darwin`/`windows` integration tests:
```bash
-make alpha-test-alpha-darwin-amd64
-make alpha-test-alpha-windows-amd64
+make test-int-validate-darwin-amd64
+make test-int-validate-windows-amd64
```
The script finds all goss spec files within `integration-tests` then filters to just ones matching the passed OS-name, then runs `validate` against them.
@@ -153,11 +154,11 @@ This is a special-case test since it requires a persistent process, then to make
#### macOS `serve`
```bash
-make "test-serve-alpha-darwin-amd64"
+make "test-int-serve-darwin-amd64"
```
#### Windows `serve`
```bash
-make "test-serve-alpha-windows-amd64"
+make "test-int-serve-windows-amd64"
```
diff --git a/examples/goss.yaml b/examples/goss.yaml
new file mode 100644
index 000000000..1265c8490
--- /dev/null
+++ b/examples/goss.yaml
@@ -0,0 +1,25 @@
+gossfile:
+ goss_awesome_gomega.yaml: {}
+
+file:
+ test.txt:
+ exists: true
+ contains: |
+ test file
+ second line
+
+command:
+ echo '15':
+ exit-status: 0
+ stdout:
+ and:
+ - gt: 10
+ - lt: 50
+ - match-regexp: '\d{2}'
+ timeout: 10000
+
+http:
+ https://ifconfig.me:
+ status: 200
+ timeout: 5000
+ body: '{{.Vars.Ip}}'
diff --git a/examples/goss_awesome_gomega.yaml b/examples/goss_awesome_gomega.yaml
new file mode 100644
index 000000000..1a9a54aa8
--- /dev/null
+++ b/examples/goss_awesome_gomega.yaml
@@ -0,0 +1,138 @@
+matching:
+ # Basic matchers
+ basic_string:
+ content: 'this is a test'
+ matches: 'this is a test'
+
+ basic_int:
+ content: 42
+ matches: 42
+
+ basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ - 'group1'
+ - 'group2'
+
+ basic_reader:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ - 'foo'
+ - '/^m.*w$/'
+ - '!wtf'
+ - '!/^ERROR:/'
+
+ # Transformers
+ basic_reader_as_array:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ and:
+ - contain-element: {contain-substring: 'foo'}
+ - contain-element: {match-regexp: '^m.*w$'}
+ - not: {contain-substring: 'wtf'}
+ - not: {match-regexp: '^ERROR:'}
+
+ test_numeric_string:
+ content: 128
+ matches:
+ and:
+ - '128'
+ - have-prefix: '1'
+ - have-suffix: '8'
+ - match-regexp: '\d{3}'
+
+ test_string_numeric:
+ content: '128'
+ matches:
+ and:
+ - 128
+ - 128.0
+ - le: 128
+ - gt: 120
+
+ test_string_float:
+ content: '128.3'
+ matches:
+ and:
+ - 128.3
+ - le: 129
+ - gt: 120.2
+
+ test_array:
+ content:
+ - '45'
+ - '46'
+ - '47'
+ matches:
+ - contain-element: {match-regexp: "4."}
+ - '45'
+ - and: [{ge: 46}, {le: 50}]
+
+ test_reader_using_string_matchers:
+ content: |
+ foo bar
+ 15
+ moo cow
+ as-reader: true
+ matches:
+ and:
+ - have-len: 19
+ - |
+ foo bar
+ 15
+ moo cow
+ - have-prefix: 'foo'
+ - have-suffix: "cow\n"
+ - contain-element:
+ have-prefix: 'moo'
+ - contain-elements:
+ - not: 'this_doesnt_exist'
+ - lt: 20
+ - have-prefix: 'moo'
+
+
+ test_reader_as_single_string:
+ content: 'cool'
+ as-reader: true
+ matches: 'cool'
+
+ test_reader_using_int_matchers:
+ content: '40'
+ as-reader: true
+ matches:
+ and:
+ - le: 250
+ - ge: 20
+
+
+ test_gjson_transform:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ moo.nested: cow
+ foo: {have-prefix: b}
+ count: {le: 25}
+ '@this': {have-key: "foo"}
+ moo:
+ and:
+ - {have-key: "nested"}
+ - {not: {have-key: "nested2"}}
+
+ test_gjson_using_this_and_equal:
+ content: '{"foo": "bar", "baz": "bing"}'
+ matches:
+ gjson:
+ '@this':
+ equal:
+ foo: bar
+ baz: bing
diff --git a/examples/readme.md b/examples/readme.md
new file mode 100644
index 000000000..350a9df18
--- /dev/null
+++ b/examples/readme.md
@@ -0,0 +1,3 @@
+# How to run this
+
+Basically, run the following: `goss --vars-inline "Ip: $EXTERNAL_IP" v`
diff --git a/examples/test.txt b/examples/test.txt
new file mode 100644
index 000000000..ae7cb70bc
--- /dev/null
+++ b/examples/test.txt
@@ -0,0 +1,2 @@
+test file
+second line
diff --git a/extras/dgoss/README.md b/extras/dgoss/README.md
index 9bdea5fd3..2282b7636 100644
--- a/extras/dgoss/README.md
+++ b/extras/dgoss/README.md
@@ -3,8 +3,8 @@
dgoss is a convenience wrapper around goss that aims to bring the simplicity of goss to docker containers.
## Examples and Tutorials
-* [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Introduction to dgoss tutorial
-* [blog tutorial](https://medium.com/@aelsabbahy/tutorial-how-to-test-your-docker-image-in-half-a-second-bbd13e06a4a9) - Same as above, but in written format
+* [blog tutorial](https://medium.com/@aelsabbahy/tutorial-how-to-test-your-docker-image-in-half-a-second-bbd13e06a4a9) - Introduction to dgoss tutorial
+* [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Same as above, but in video format
* [dgoss-examples](https://github.com/aelsabbahy/dgoss-examples) - Repo containing examples of using dgoss to validate docker images
## Installation
diff --git a/go.mod b/go.mod
index 6d0443259..685486c92 100644
--- a/go.mod
+++ b/go.mod
@@ -9,18 +9,22 @@ require (
github.com/cheekybits/genny v1.0.0
github.com/fatih/color v1.15.0
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d
- github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b
+ github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5
github.com/hashicorp/logutils v1.0.0
+ github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0
github.com/miekg/dns v1.1.52
github.com/moby/sys/mountinfo v0.6.2
github.com/oleiade/reflections v1.0.1
github.com/onsi/gomega v1.27.4
github.com/patrickmn/go-cache v2.1.0+incompatible
- github.com/prometheus/client_golang v1.14.0
- github.com/prometheus/common v0.42.0
+ github.com/prometheus/client_golang v1.7.1
+ github.com/prometheus/common v0.10.0
+ github.com/samber/lo v1.38.1
github.com/stretchr/testify v1.8.1
+ github.com/tidwall/gjson v1.14.4
github.com/urfave/cli v1.22.12
gopkg.in/yaml.v2 v2.4.0
+ gotest.tools/v3 v3.0.3
)
require (
@@ -40,13 +44,17 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/pkg/errors v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
diff --git a/go.sum b/go.sum
index 26d18c361..7a4f00169 100644
--- a/go.sum
+++ b/go.sum
@@ -7,10 +7,17 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 h1:NYoPVh1XuUB5VBWLXRKoqzQhl4bajIxh+XuURbJ0uwc=
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2/go.mod h1:DCNKSpXhum14Y258jSbRmJvcesbzEdBPincz7yJUx3k=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
@@ -20,50 +27,73 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
-github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d h1:50mlZKtg8BUvBtFs0ioVpSgMMwcKaJefg/2pZ+lQf98=
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d/go.mod h1:MBdRlloGIbpQVDuH5Gxg3hjqwZBCZsmFqbYPaeR6r0M=
-github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b h1:IVeeIVrdgyMG817dquwmYwcB9TFwphQAhLOiJ5lt8Nc=
-github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b/go.mod h1:5oH/hhdJNuFnJTAoXErJ2Jr8LOkbkzosMZT93MrlMOc=
+github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 h1:NW0Jo4leMIrQxNOyOkBu4yBnygI37m0Ey0EUUgvzr+8=
+github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5/go.mod h1:FYj70SLmogHdTTDGnIVaaK0iczROlsxmoMCwfAUuIE8=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 h1:nHoRIX8iXob3Y2kdt9KsjyIb7iApSvb3vgsd93xb5Ow=
+github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
-github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
-github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -74,95 +104,114 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
-github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow=
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
-github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
-github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
-github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
-github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
-github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
-github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
-github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -170,38 +219,41 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
diff --git a/goss_test.go b/goss_test.go
index a43d3f72a..16f23084d 100644
--- a/goss_test.go
+++ b/goss_test.go
@@ -5,9 +5,9 @@ import (
"encoding/json"
"os"
"testing"
- "time"
"github.com/goss-org/goss/outputs"
+ "github.com/goss-org/goss/resource"
"github.com/goss-org/goss/util"
)
@@ -84,13 +84,13 @@ func TestUseAsPackage(t *testing.T) {
for _, r := range rg {
found++
- if r.Successful {
+ if r.Result == resource.SUCCESS {
passed++
}
}
}
- code, err := Validate(cfg, time.Now())
+ code, err := Validate(cfg)
checkErr(t, err, "check failed")
if code != 0 {
t.Fatalf("check failed, expected 0 got %d", code)
@@ -110,7 +110,7 @@ func TestUseAsPackage(t *testing.T) {
okcount := 0
for _, r := range res.Results {
- if r.Successful {
+ if r.Result == resource.SUCCESS {
okcount++
}
}
@@ -149,7 +149,7 @@ func TestSkipResourcesByType(t *testing.T) {
}
}
- if skipped != 6 {
- t.Fatalf("Expected to skip 6 tests, skipped %d", skipped)
+ if skipped != 5 {
+ t.Fatalf("Expected to skip 5 tests, skipped %d", skipped)
}
}
diff --git a/install.sh b/install.sh
index d1c8d8c26..365a8be3b 100644
--- a/install.sh
+++ b/install.sh
@@ -3,8 +3,9 @@
{
set -e
-LATEST_URL="https://github.com/goss-org/goss/releases/latest"
-LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}')
+# LATEST_URL="https://github.com/goss-org/goss/releases/latest"
+# LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}')
+LATEST_EFFECTIVE=https://github.com/goss-org/goss/releases/tag/v0.3.23
LATEST=${LATEST_EFFECTIVE##*/}
DGOSS_VER=$GOSS_VER
diff --git a/integration-tests/Dockerfile_trusty b/integration-tests/Dockerfile_trusty
index 975b39eb0..04e0f8235 100644
--- a/integration-tests/Dockerfile_trusty
+++ b/integration-tests/Dockerfile_trusty
@@ -4,8 +4,7 @@ MAINTAINER Ahmed
RUN apt-get update && \
apt-get install -y apache2=2.4.7-1ubuntu4.22 tinyproxy && \
apt-get remove -y vim-tiny && \
- apt-get clean && \
- echo manual > /etc/init/apache2.override
+ apt-get clean
RUN sed -i '/reload|force-reload)/i status) pidof tinyproxy > /dev/null && echo "tinyproxy is running";;' /etc/init.d/tinyproxy
RUN sed -i '/start)/a\ touch /var/log/tinyproxy/tinyproxy.log /var/run/tinyproxy/tinyproxy.pid' /etc/init.d/tinyproxy
diff --git a/integration-tests/goss/alpine3/goss-expected-q.yaml b/integration-tests/goss/alpine3/goss-expected-q.yaml
index 9aa197cbd..e6f17cf03 100644
--- a/integration-tests/goss/alpine3/goss-expected-q.yaml
+++ b/integration-tests/goss/alpine3/goss-expected-q.yaml
@@ -1,10 +1,10 @@
file:
/etc/passwd:
exists: true
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -111,16 +111,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/alpine3/goss-expected.yaml b/integration-tests/goss/alpine3/goss-expected.yaml
index 99cef1789..27da2b829 100644
--- a/integration-tests/goss/alpine3/goss-expected.yaml
+++ b/integration-tests/goss/alpine3/goss-expected.yaml
@@ -2,14 +2,13 @@ file:
/etc/passwd:
exists: true
mode: "0644"
- size: 1287
owner: root
group: root
filetype: file
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -154,16 +153,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/centos7/goss-aa-expected.yaml b/integration-tests/goss/centos7/goss-aa-expected.yaml
index 13ba204dc..a7aa4ce4d 100644
--- a/integration-tests/goss/centos7/goss-aa-expected.yaml
+++ b/integration-tests/goss/centos7/goss-aa-expected.yaml
@@ -2,7 +2,7 @@ package:
httpd:
installed: true
versions:
- - 2.4.6
+ - 2.4.6-95.el7.centos
port:
tcp:80:
listening: true
diff --git a/integration-tests/goss/centos7/goss-expected-q.yaml b/integration-tests/goss/centos7/goss-expected-q.yaml
index 345c983e8..349eda8f9 100644
--- a/integration-tests/goss/centos7/goss-expected-q.yaml
+++ b/integration-tests/goss/centos7/goss-expected-q.yaml
@@ -1,10 +1,10 @@
file:
/etc/passwd:
exists: true
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
foobar:
installed: false
@@ -111,16 +111,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/centos7/goss-expected.yaml b/integration-tests/goss/centos7/goss-expected.yaml
index 1d82b5da3..8db6d4646 100644
--- a/integration-tests/goss/centos7/goss-expected.yaml
+++ b/integration-tests/goss/centos7/goss-expected.yaml
@@ -2,21 +2,20 @@ file:
/etc/passwd:
exists: true
mode: "0644"
- size: 810
owner: root
group: root
filetype: file
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
foobar:
installed: false
httpd:
installed: true
versions:
- - 2.4.6
+ - 2.4.6-95.el7.centos
vim-tiny:
installed: false
addr:
@@ -160,16 +159,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/centos7/goss.yaml b/integration-tests/goss/centos7/goss.yaml
index a48058ab2..818b2c34d 100644
--- a/integration-tests/goss/centos7/goss.yaml
+++ b/integration-tests/goss/centos7/goss.yaml
@@ -1,4 +1,3 @@
----
service:
autofs:
enabled: false
@@ -32,4 +31,3 @@ gossfile:
"../goss-s*.yaml": {}
bypath:
file: "../goss-dummy.yaml"
-
diff --git a/integration-tests/goss/darwin/commands/add.goss.yaml b/integration-tests/goss/darwin/commands/add.goss.yaml
index c07a27f90..cd75e38cc 100644
--- a/integration-tests/goss/darwin/commands/add.goss.yaml
+++ b/integration-tests/goss/darwin/commands/add.goss.yaml
@@ -3,7 +3,7 @@
command:
"add addr 127.0.0.1":
exit-status: 0
- exec: release/goss-alpha-darwin-amd64 --use-alpha=1 add addr 127.0.0.1
+ exec: release/goss-darwin-amd64 --use-alpha=1 add addr 127.0.0.1
stdout:
- "timeout: 500"
stderr: []
diff --git a/integration-tests/goss/darwin/commands/autoadd.goss.yaml b/integration-tests/goss/darwin/commands/autoadd.goss.yaml
index f2243cf92..830244ba4 100644
--- a/integration-tests/goss/darwin/commands/autoadd.goss.yaml
+++ b/integration-tests/goss/darwin/commands/autoadd.goss.yaml
@@ -2,7 +2,7 @@
command:
"autoadd /Users/travis":
exit-status: 0
- exec: "release/goss-alpha-darwin-amd64 --use-alpha=1 autoadd /Users/travis"
+ exec: "release/goss-darwin-amd64 --use-alpha=1 autoadd /Users/travis"
stdout:
- 'file:'
- ' exists: true'
diff --git a/integration-tests/goss/darwin/commands/help.goss.yaml b/integration-tests/goss/darwin/commands/help.goss.yaml
index 7af8f2740..d57d2bb64 100644
--- a/integration-tests/goss/darwin/commands/help.goss.yaml
+++ b/integration-tests/goss/darwin/commands/help.goss.yaml
@@ -2,7 +2,7 @@
command:
help:
exit-status: 0
- exec: "release/goss-alpha-darwin-amd64 help"
+ exec: "release/goss-darwin-amd64 help"
stdout:
- alpha
stderr: []
diff --git a/integration-tests/goss/darwin/commands/validate.goss.yaml b/integration-tests/goss/darwin/commands/validate.goss.yaml
index edf22cad6..6d79a6d68 100644
--- a/integration-tests/goss/darwin/commands/validate.goss.yaml
+++ b/integration-tests/goss/darwin/commands/validate.goss.yaml
@@ -3,7 +3,7 @@
command:
"validate":
exit-status: 0
- exec: "release/goss-alpha-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate"
+ exec: "release/goss-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate"
stdout:
- 'Count: 1'
- 'Failed: 0'
diff --git a/integration-tests/goss/darwin/tests/file.goss.yaml b/integration-tests/goss/darwin/tests/file.goss.yaml
index 0f428a235..347494ea4 100644
--- a/integration-tests/goss/darwin/tests/file.goss.yaml
+++ b/integration-tests/goss/darwin/tests/file.goss.yaml
@@ -9,6 +9,6 @@ file:
filetype: file
md5: 9dcea4037b1439a2a96e4d206eda63a4
sha256: e73d885411a52a0d29142e830e104e0cc9252fbb1dc3c92a430ef7c369f089ef
- contains:
+ contents:
- "nothing to see here"
- "/nothing.*here/"
diff --git a/integration-tests/goss/generate_goss.sh b/integration-tests/goss/generate_goss.sh
index 0cdfc3daa..93e822fb6 100755
--- a/integration-tests/goss/generate_goss.sh
+++ b/integration-tests/goss/generate_goss.sh
@@ -59,10 +59,14 @@ goss a "${args[@]}" process $package foobar
goss a "${args[@]}" kernel-param kernel.ostype
goss a "${args[@]}" mount /dev
+# Make tests consistent across different docker setups
+sed -i '/- seclabel/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
+sed -i '/- size=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
+sed -i '/- mode=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
goss a "${args[@]}" http https://www.google.com
-goss a "${args[@]}" http https://www.microsoft.com -x http://127.0.0.1:8888
+goss a "${args[@]}" http https://www.apple.com -x http://127.0.0.1:8888
goss a "${args[@]}" http http://google.com -r
@@ -78,6 +82,7 @@ $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.ya
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package
#Â Validate that we can aa none existent resources without destroying the file
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource
+
if [[ ! -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]]
then
echo "Error! Config file removed by aa!" && exit 1
diff --git a/integration-tests/goss/goss-shared.yaml b/integration-tests/goss/goss-shared.yaml
index 9b794bb20..ce5a1f5e7 100644
--- a/integration-tests/goss/goss-shared.yaml
+++ b/integration-tests/goss/goss-shared.yaml
@@ -25,7 +25,7 @@ file:
owner: root
group: root
filetype: file
- contains:
+ contents:
- root
{{end}}
"/goss/hellogoss.txt":
@@ -35,7 +35,7 @@ file:
sha512: 372864ab83187de41ca57c5c77cd4a99220ccadc8b8ddb18367893fd3e58764193a599edbf63a48c0c44f1e923606a00929b46de3bda1744fd722b9d42829206
"/tmp/goss/foobar":
exists: false
- contains: []
+ contents: []
"~root":
exists: true
mode: '0700'
@@ -50,7 +50,7 @@ file:
filetype: pipe
"/does/not/exist":
exists: true
- contains:
+ contents:
- skip-this-test
skip: true
package:
@@ -62,6 +62,13 @@ package:
versions:
- {{$ver}}
{{end}}
+service:
+{{- range $name, $runlevels := index .Vars .Env.OS "services"}}
+ {{$name}}:
+ enabled: true
+ running: true
+ runlevels: {{toJson $runlevels}}
+{{end}}
addr:
tcp://google.com:22:
reachable: false
@@ -165,6 +172,7 @@ mount:
opts:
- rw
- nosuid
+ - mode=755
source: tmpfs
filesystem: tmpfs
"/":
@@ -183,12 +191,13 @@ http:
{{ if index .Vars .Env.OS "proxy" }}
https://httpbin.org/anything:
status: 200
+ timeout: 60000
proxy: {{ index .Vars .Env.OS "proxy" }}
{{ end }}
https://httpbin.org/headers:
status: 200
allow-insecure: false
- timeout: 5000
+ timeout: 60000
request-headers:
- "Foo: bar"
headers: ["Content-Type: application/json"]
@@ -196,7 +205,7 @@ http:
https://httpbin.org/headers?host:
status: 200
allow-insecure: false
- timeout: 5000
+ timeout: 60000
request-headers:
# This is causing intermittent errors depending on the httpbin server hit
# need to see if there's a good way around this, maybe local httpbin?
@@ -207,26 +216,31 @@ http:
body: ['"Host": "httpbin.org"']
https://httpbin.org/basic-auth/username/secret:
status: 200
+ timeout: 60000
username: username
password: secret
https://httpbin.org/basic-auth/username/secret?failure:
status: 401
+ timeout: 60000
username: username
password: wrong
https://httpbin.org/put:
status: 200
method: PUT
+ timeout: 60000
request-body: '{"key": "value"}'
body:
- '"key": "value"'
anything-with-get:
url: https://httpbin.org/anything
status: 200
+ timeout: 60000
body: []
anything-with-put:
url: https://httpbin.org/anything
status: 200
method: GET
+ timeout: 60000
request-body: "request-body"
body: ["request-body"]
matching:
@@ -245,8 +259,6 @@ matching:
baz: bing
matches:
and:
- - have-key-with-value:
- foo: bar
- have-key: baz
semver:
content:
diff --git a/integration-tests/goss/trusty/goss-aa-expected.yaml b/integration-tests/goss/trusty/goss-aa-expected.yaml
index a9a98ed27..8be205cea 100644
--- a/integration-tests/goss/trusty/goss-aa-expected.yaml
+++ b/integration-tests/goss/trusty/goss-aa-expected.yaml
@@ -10,7 +10,7 @@ port:
- 0.0.0.0
service:
apache2:
- enabled: false
+ enabled: true
running: true
process:
apache2:
diff --git a/integration-tests/goss/trusty/goss-expected-q.yaml b/integration-tests/goss/trusty/goss-expected-q.yaml
index c1e6e053a..9554f8e82 100644
--- a/integration-tests/goss/trusty/goss-expected-q.yaml
+++ b/integration-tests/goss/trusty/goss-expected-q.yaml
@@ -1,10 +1,10 @@
file:
/etc/passwd:
exists: true
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -31,7 +31,7 @@ port:
listening: false
service:
apache2:
- enabled: false
+ enabled: true
running: true
foobar:
enabled: false
@@ -111,16 +111,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/trusty/goss-expected.yaml b/integration-tests/goss/trusty/goss-expected.yaml
index 9aad00c7d..94e590364 100644
--- a/integration-tests/goss/trusty/goss-expected.yaml
+++ b/integration-tests/goss/trusty/goss-expected.yaml
@@ -2,14 +2,13 @@ file:
/etc/passwd:
exists: true
mode: "0644"
- size: 1006
owner: root
group: root
filetype: file
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -42,7 +41,7 @@ port:
ip: []
service:
apache2:
- enabled: false
+ enabled: true
running: true
foobar:
enabled: false
@@ -160,16 +159,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/vars.yaml b/integration-tests/goss/vars.yaml
index ec08626b9..cbf6df778 100644
--- a/integration-tests/goss/vars.yaml
+++ b/integration-tests/goss/vars.yaml
@@ -3,19 +3,27 @@ alpine3:
proxy: http://127.0.0.1:8888
packages:
apache2: "2.4.46-r1"
+ services:
+ apache2: [sysinit]
arch:
packages:
centos7:
proxy: http://127.0.0.1:8888
packages:
- httpd: "2.4.6"
+ httpd: "2.4.6-95.el7.centos"
+ services:
+ httpd: []
trusty:
proxy: http://127.0.0.1:8888
packages:
apache2: "2.4.7-1ubuntu4.22"
+ services:
+ apache2: ["3"]
wheezy:
proxy: http://127.0.0.1:8888
packages:
apache2: "2.2.22-13+deb7u13"
+ services:
+ apache2: ["2", "3", "5", "4"]
overwrite: foo
diff --git a/integration-tests/goss/wheezy/goss-expected-q.yaml b/integration-tests/goss/wheezy/goss-expected-q.yaml
index e37715a76..9554f8e82 100644
--- a/integration-tests/goss/wheezy/goss-expected-q.yaml
+++ b/integration-tests/goss/wheezy/goss-expected-q.yaml
@@ -1,10 +1,10 @@
file:
/etc/passwd:
exists: true
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -111,16 +111,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/wheezy/goss-expected.yaml b/integration-tests/goss/wheezy/goss-expected.yaml
index d500dff16..27295fbe9 100644
--- a/integration-tests/goss/wheezy/goss-expected.yaml
+++ b/integration-tests/goss/wheezy/goss-expected.yaml
@@ -2,14 +2,13 @@ file:
/etc/passwd:
exists: true
mode: "0644"
- size: 812
owner: root
group: root
filetype: file
- contains: []
+ contents: []
/tmp/goss/foobar:
exists: false
- contains: []
+ contents: []
package:
apache2:
installed: true
@@ -160,16 +159,16 @@ http:
no-follow-redirects: true
timeout: 5000
body: []
- https://www.google.com:
+ https://www.apple.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- https://www.microsoft.com:
+ proxy: http://127.0.0.1:8888
+ https://www.google.com:
status: 200
allow-insecure: false
no-follow-redirects: false
timeout: 5000
body: []
- proxy: http://127.0.0.1:8888
diff --git a/integration-tests/goss/windows/commands/add.goss.yaml b/integration-tests/goss/windows/commands/add.goss.yaml
index 8b4784895..d65d1899f 100644
--- a/integration-tests/goss/windows/commands/add.goss.yaml
+++ b/integration-tests/goss/windows/commands/add.goss.yaml
@@ -3,7 +3,7 @@
command:
"add addr 127.0.0.1":
exit-status: 0
- exec: release\goss-alpha-windows-amd64 --use-alpha=1 add addr 127.0.0.1
+ exec: release\goss-windows-amd64 --use-alpha=1 add addr 127.0.0.1
stdout:
- "timeout: 500"
stderr: []
diff --git a/integration-tests/goss/windows/commands/autoadd.goss.yaml b/integration-tests/goss/windows/commands/autoadd.goss.yaml
index cd88a70dc..5ed311a2b 100644
--- a/integration-tests/goss/windows/commands/autoadd.goss.yaml
+++ b/integration-tests/goss/windows/commands/autoadd.goss.yaml
@@ -2,7 +2,7 @@
command:
"autoadd Administrator":
exit-status: 0
- exec: release\goss-alpha-windows-amd64 --use-alpha=1 autoadd Administrator
+ exec: release\goss-windows-amd64 --use-alpha=1 autoadd Administrator
stdout:
- 'user:'
- ' name: Administrator'
diff --git a/integration-tests/goss/windows/commands/help.goss.yaml b/integration-tests/goss/windows/commands/help.goss.yaml
index d1c5463bc..2f8bce527 100644
--- a/integration-tests/goss/windows/commands/help.goss.yaml
+++ b/integration-tests/goss/windows/commands/help.goss.yaml
@@ -2,7 +2,7 @@
command:
help:
exit-status: 0
- exec: release\goss-alpha-windows-amd64 help
+ exec: release\goss-windows-amd64 help
stdout:
- alpha
stderr: []
diff --git a/integration-tests/goss/windows/commands/validate.goss.yaml b/integration-tests/goss/windows/commands/validate.goss.yaml
index c504b5ec7..fe2456acd 100644
--- a/integration-tests/goss/windows/commands/validate.goss.yaml
+++ b/integration-tests/goss/windows/commands/validate.goss.yaml
@@ -3,7 +3,7 @@
command:
"validate":
exit-status: 0
- exec: "release\\goss-alpha-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate"
+ exec: "release\\goss-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate"
stdout:
- 'Count: 1'
- 'Failed: 0'
diff --git a/integration-tests/goss/windows/tests/command.goss.yaml b/integration-tests/goss/windows/tests/command.goss.yaml
index 26a48dcf1..e5955ed2f 100644
--- a/integration-tests/goss/windows/tests/command.goss.yaml
+++ b/integration-tests/goss/windows/tests/command.goss.yaml
@@ -11,14 +11,14 @@ command:
exec: powershell -noprofile -noninteractive -command (get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous
exit-status: 0
stdout:
- - 0
+ - "0"
stderr: []
timeout: 10000
wrap a powershell with quotes - expect 0 because travis does not restrict anonymous logins:
exec: powershell -noprofile -noninteractive -command "(get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous"
exit-status: 0
stdout:
- - 0
+ - "0"
stderr: []
timeout: 10000
powershell with quotes:
diff --git a/integration-tests/goss/windows/tests/file.goss.yaml b/integration-tests/goss/windows/tests/file.goss.yaml
index 80f1b7508..a1c3ed0b4 100644
--- a/integration-tests/goss/windows/tests/file.goss.yaml
+++ b/integration-tests/goss/windows/tests/file.goss.yaml
@@ -9,6 +9,6 @@ file:
filetype: file
md5: dc9a07ca9789f866d21d544fe5651954
sha256: aa8b1b4a0d9bf174f5019c8f8a9568858ee2bdf8e0ad16aec54417d49b48df49
- contains:
+ contents:
- "nothing to see here"
- "/nothing.*here/"
diff --git a/integration-tests/run-serve-tests.sh b/integration-tests/run-serve-tests.sh
index e9c072918..33f8865c6 100755
--- a/integration-tests/run-serve-tests.sh
+++ b/integration-tests/run-serve-tests.sh
@@ -9,10 +9,6 @@ IFS='- ' read -r -a segments <<< "${platform_spec}"
os="${segments[0]}"
arch="${segments[1]}"
-if [[ "${segments[0]}" == "alpha" ]]; then
- os="${segments[1]}"
- arch="${segments[2]}"
-fi
find_open_port() {
local startAt="${1:?"Supply start of port range"}"
diff --git a/integration-tests/run-validate-tests.sh b/integration-tests/run-validate-tests.sh
new file mode 100755
index 000000000..90f85c207
--- /dev/null
+++ b/integration-tests/run-validate-tests.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+# shellcheck source=../ci/lib/setup.sh
+source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67
+
+platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}"
+# Split platform_spec into platform/arch segments
+IFS='- ' read -r -a segments <<< "${platform_spec}"
+
+os="${segments[0]}"
+arch="${segments[1]}"
+
+if [[ "${os}" == "linux" ]]; then
+ echo "OS is ${os}. This script is not for running tests on the different flavours of linux."
+ echo "Linux is exercised via the integration-tests/test.sh currently, because linux can be"
+ echo "verified via docker containers; macOS and Windows cannot."
+ echo "This script is for macOS and Windows, and runs tests that are expected to pass on"
+ echo "Travis-CI provided images, running nakedly (no containerisation) on the hosts there."
+ exit 1
+fi
+
+repo_root="$(git rev-parse --show-toplevel)"
+export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}"
+log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}"
+readarray -t goss_test_files < <(find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq)
+
+export GOSS_USE_ALPHA=1
+for file in "${goss_test_files[@]}"; do
+ args=(
+ "-g=${file}"
+ "validate"
+ )
+ log_action -e "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n"
+ "${GOSS_BINARY}" "${args[@]}"
+done
diff --git a/integration-tests/test.sh b/integration-tests/test.sh
index 7a354e2a7..ed941cac9 100755
--- a/integration-tests/test.sh
+++ b/integration-tests/test.sh
@@ -9,19 +9,6 @@ arch="${2:?"Need arch as 2nd arg. e.g. amd64 386"}"
vars_inline="{inline: bar, overwrite: bar}"
-seccomp_opts() {
- local docker_ver minor_ver
- docker_ver=$(docker version -f '{{.Client.Version}}')
- minor_ver=$(cut -d'.' -f2 <<<$docker_ver)
- major_ver=$(cut -d'.' -f1 <<<$docker_ver)
- if ((minor_ver>=10))||((major_ver>18)); then
- echo ' --security-opt seccomp:unconfined '
- fi
- if ((major_ver>18)); then
- echo ' --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro '
- fi
-}
-
# setup places us inside repo-root; this preserves current behaviour with least change.
cd integration-tests
@@ -43,7 +30,7 @@ docker_exec() {
if docker ps -a | grep "$container_name";then
docker rm -vf "$container_name"
fi
-opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" $(seccomp_opts))
+opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" --security-opt seccomp:unconfined --security-opt label:disable)
id=$(docker run "${opts[@]}" "aelsabbahy/goss_$os" /sbin/init)
ip=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id")
trap "rv=\$?; docker rm -vf $id; exit \$rv" INT TERM EXIT
@@ -57,7 +44,7 @@ echo "$out"
if [[ $os == "arch" ]]; then
egrep -q 'Count: 99, Failed: 0, Skipped: 3' <<<"$out"
else
- egrep -q 'Count: 119, Failed: 0, Skipped: 5' <<<"$out"
+ egrep -q 'Count: 120, Failed: 0, Skipped: 5' <<<"$out"
fi
if [[ ! $os == "arch" ]]; then
diff --git a/matcher_test.go b/matcher_test.go
new file mode 100644
index 000000000..64703307f
--- /dev/null
+++ b/matcher_test.go
@@ -0,0 +1,86 @@
+//go:build linux
+
+package goss
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/goss-org/goss/util"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ // This will generate the "golden files" prior to running the tests.
+ // helpful when the output is changed and a user doesn't want to update every single expectation file by hand
+ update = flag.Bool("update", false, "update the golden files of this test")
+)
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ os.Exit(m.Run())
+}
+
+func TestMatchers(t *testing.T) {
+ files, err := filepath.Glob(filepath.Join("testdata", "out_matching_*"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, outFile := range files {
+ outFile := outFile
+ parts := strings.Split(outFile, ".")
+ specName := fmt.Sprintf("%s.yaml", strings.TrimPrefix(parts[0], "testdata/out_"))
+ specFile := filepath.Join("testdata", specName)
+ outFormat := parts[2]
+ wantCode, err := strconv.Atoi(parts[1])
+ if err != nil {
+ t.Fatal(err)
+ }
+ tn := outFile
+ t.Run(tn, func(t *testing.T) {
+ output := &bytes.Buffer{}
+
+ cfg, err := util.NewConfig(
+ util.WithOutputFormat(outFormat),
+ util.WithResultWriter(output),
+ util.WithSpecFile(specFile),
+ util.WithFormatOptions("sort", "pretty"),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ exitCode, err := Validate(cfg)
+ actualOut := output.String()
+ actualOut = sanitizeOutput(actualOut)
+
+ if *update {
+ os.WriteFile(outFile, []byte(actualOut), 0644)
+ }
+ wantOutB, err := os.ReadFile(outFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantOut := string(wantOutB)
+ if actualOut != wantOut {
+ assert.Equal(t, wantOut, actualOut)
+ }
+ if exitCode != wantCode {
+ assert.Equal(t, wantCode, exitCode)
+ }
+ })
+ }
+}
+
+func sanitizeOutput(s string) string {
+ // Remove duration time
+ re := regexp.MustCompile(`\d\.\d\d\ds`)
+ return re.ReplaceAllString(s, "")
+}
diff --git a/matchers/and.go b/matchers/and.go
new file mode 100644
index 000000000..502083bd5
--- /dev/null
+++ b/matchers/and.go
@@ -0,0 +1,50 @@
+package matchers
+
+import (
+ "encoding/json"
+)
+
+type AndMatcher struct {
+ fakeOmegaMatcher
+ Matchers []GossMatcher
+
+ // state
+ firstFailedMatcher GossMatcher
+}
+
+func And(ms ...GossMatcher) GossMatcher {
+ return &AndMatcher{Matchers: ms}
+}
+
+func (m *AndMatcher) Match(actual interface{}) (success bool, err error) {
+ m.firstFailedMatcher = nil
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if !success || err != nil {
+ m.firstFailedMatcher = matcher
+ return false, err
+ }
+ }
+ return true, nil
+}
+
+func (m *AndMatcher) FailureResult(actual interface{}) MatcherResult {
+ return m.firstFailedMatcher.FailureResult(actual)
+}
+
+func (m *AndMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to satisfy all of these matchers",
+ Expected: m.Matchers,
+ }
+}
+
+func (m *AndMatcher) MarshalJSON() ([]byte, error) {
+ if len(m.Matchers) == 1 {
+ return json.Marshal(m.Matchers[0])
+ }
+ j := make(map[string]interface{})
+ j["and"] = m.Matchers
+ return json.Marshal(j)
+}
diff --git a/matchers/be_numerically_matcher.go b/matchers/be_numerically_matcher.go
new file mode 100644
index 000000000..ca23030ad
--- /dev/null
+++ b/matchers/be_numerically_matcher.go
@@ -0,0 +1,68 @@
+package matchers
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type BeNumericallyMatcher struct {
+ fakeOmegaMatcher
+ Comparator string
+ CompareTo []interface{}
+}
+
+func BeNumerically(comparator string, compareTo ...interface{}) GossMatcher {
+ return &BeNumericallyMatcher{
+ Comparator: comparator,
+ CompareTo: compareTo,
+ }
+}
+func (m *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) {
+ comparator, err := strToSymbol(m.Comparator)
+ if err != nil {
+ return false, err
+ }
+ matcher := &matchers.BeNumericallyMatcher{
+ Comparator: comparator,
+ CompareTo: m.CompareTo,
+ }
+ return matcher.Match(actual)
+}
+
+func (m *BeNumericallyMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: fmt.Sprintf("to be numerically %s", m.Comparator),
+ Expected: m.CompareTo[0],
+ }
+}
+
+func (m *BeNumericallyMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: fmt.Sprintf("not to be numerically %s", m.Comparator),
+ Expected: m.CompareTo[0],
+ }
+}
+
+func (m *BeNumericallyMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j[m.Comparator] = m.CompareTo[0]
+ return json.Marshal(j)
+}
+
+func strToSymbol(s string) (string, error) {
+ comparator, ok := map[string]string{
+ "gt": ">",
+ "ge": ">=",
+ "lt": "<",
+ "le": "<=",
+ "eq": "==",
+ }[s]
+ if !ok {
+ return "", fmt.Errorf("Unknown comparator: %s", s)
+ }
+ return comparator, nil
+}
diff --git a/matchers/consist_of.go b/matchers/consist_of.go
new file mode 100644
index 000000000..94a114011
--- /dev/null
+++ b/matchers/consist_of.go
@@ -0,0 +1,45 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type ConsistOfMatcher struct {
+ matchers.ConsistOfMatcher
+}
+
+func ConsistOf(elements ...interface{}) GossMatcher {
+ return &ConsistOfMatcher{
+ matchers.ConsistOfMatcher{
+ Elements: elements,
+ },
+ }
+}
+
+func (m *ConsistOfMatcher) FailureResult(actual interface{}) MatcherResult {
+ missingElements := getUnexported(m, "missingElements")
+ extraElements := getUnexported(m, "extraElements")
+ return MatcherResult{
+ Actual: actual,
+ Message: "to consist of",
+ Expected: m.Elements,
+ MissingElements: missingElements,
+ ExtraElements: extraElements,
+ }
+}
+
+func (m *ConsistOfMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to consist of",
+ Expected: m.Elements,
+ }
+}
+
+func (m *ConsistOfMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["consist-of"] = m.Elements
+ return json.Marshal(j)
+}
diff --git a/matchers/contain_element_matcher.go b/matchers/contain_element_matcher.go
new file mode 100644
index 000000000..30a125e1f
--- /dev/null
+++ b/matchers/contain_element_matcher.go
@@ -0,0 +1,41 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type ContainElementMatcher struct {
+ matchers.ContainElementMatcher
+}
+
+func ContainElement(element interface{}) GossMatcher {
+ return &ContainElementMatcher{
+ matchers.ContainElementMatcher{
+ Element: element,
+ },
+ }
+}
+
+func (m *ContainElementMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to contain element matching",
+ Expected: m.Element,
+ }
+}
+
+func (m *ContainElementMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to contain element matching",
+ Expected: m.Element,
+ }
+}
+
+func (m *ContainElementMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["contain-element"] = m.Element
+ return json.Marshal(j)
+}
diff --git a/matchers/contain_elements_matcher.go b/matchers/contain_elements_matcher.go
new file mode 100644
index 000000000..1d478594f
--- /dev/null
+++ b/matchers/contain_elements_matcher.go
@@ -0,0 +1,43 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type ContainElementsMatcher struct {
+ matchers.ContainElementsMatcher
+}
+
+func ContainElements(elements ...interface{}) GossMatcher {
+ return &ContainElementsMatcher{
+ matchers.ContainElementsMatcher{
+ Elements: elements,
+ },
+ }
+}
+func (m *ContainElementsMatcher) FailureResult(actual interface{}) MatcherResult {
+ missingElements := getUnexported(m, "missingElements")
+ return MatcherResult{
+ Actual: actual,
+ Message: "to contain elements",
+ Expected: m.Elements,
+ MissingElements: missingElements,
+ }
+
+}
+func (m *ContainElementsMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to contain elements",
+ Expected: m.Elements,
+ }
+
+}
+
+func (m *ContainElementsMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["contain-elements"] = m.Elements
+ return json.Marshal(j)
+}
diff --git a/matchers/contain_substring_matcher.go b/matchers/contain_substring_matcher.go
new file mode 100644
index 000000000..851218d84
--- /dev/null
+++ b/matchers/contain_substring_matcher.go
@@ -0,0 +1,42 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type ContainSubstringMatcher struct {
+ matchers.ContainSubstringMatcher
+}
+
+func ContainSubstring(substr string, args ...interface{}) GossMatcher {
+ return &ContainSubstringMatcher{
+ matchers.ContainSubstringMatcher{
+ Substr: substr,
+ Args: args,
+ },
+ }
+}
+
+func (m *ContainSubstringMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to contain substring",
+ Expected: m.Substr,
+ }
+}
+
+func (m *ContainSubstringMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to contain substring",
+ Expected: m.Substr,
+ }
+}
+
+func (m *ContainSubstringMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["contain-substring"] = m.Substr
+ return json.Marshal(j)
+}
diff --git a/matchers/equal_matcher.go b/matchers/equal_matcher.go
new file mode 100644
index 000000000..faa2125ea
--- /dev/null
+++ b/matchers/equal_matcher.go
@@ -0,0 +1,39 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type EqualMatcher struct {
+ matchers.EqualMatcher
+}
+
+func Equal(element interface{}) GossMatcher {
+ return &EqualMatcher{
+ matchers.EqualMatcher{
+ Expected: element,
+ },
+ }
+}
+
+func (m *EqualMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to equal",
+ Expected: m.Expected,
+ }
+}
+
+func (m *EqualMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to equal",
+ Expected: m.Expected,
+ }
+}
+
+func (m *EqualMatcher) MarshalJSON() ([]byte, error) {
+ return json.Marshal(m.Expected)
+}
diff --git a/matchers/have_key_matcher.go b/matchers/have_key_matcher.go
new file mode 100644
index 000000000..624203c29
--- /dev/null
+++ b/matchers/have_key_matcher.go
@@ -0,0 +1,41 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type HaveKeyMatcher struct {
+ matchers.HaveKeyMatcher
+}
+
+func HaveKey(key interface{}) GossMatcher {
+ return &HaveKeyMatcher{
+ matchers.HaveKeyMatcher{
+ Key: key,
+ },
+ }
+}
+
+func (m *HaveKeyMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to have key matching",
+ Expected: m.Key,
+ }
+}
+
+func (m *HaveKeyMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to have key matching",
+ Expected: m.Key,
+ }
+}
+
+func (m *HaveKeyMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["have-key"] = m.Key
+ return json.Marshal(j)
+}
diff --git a/matchers/have_len_matcher.go b/matchers/have_len_matcher.go
new file mode 100644
index 000000000..f6b1038e2
--- /dev/null
+++ b/matchers/have_len_matcher.go
@@ -0,0 +1,41 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type HaveLenMatcher struct {
+ matchers.HaveLenMatcher
+}
+
+func HaveLen(count int) GossMatcher {
+ return &HaveLenMatcher{
+ matchers.HaveLenMatcher{
+ Count: count,
+ },
+ }
+}
+
+func (m *HaveLenMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to have length",
+ Expected: m.Count,
+ }
+}
+
+func (m *HaveLenMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to have length",
+ Expected: m.Count,
+ }
+}
+
+func (m *HaveLenMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["have-len"] = m.Count
+ return json.Marshal(j)
+}
diff --git a/matchers/have_patterns.go b/matchers/have_patterns.go
new file mode 100644
index 000000000..cff7032f1
--- /dev/null
+++ b/matchers/have_patterns.go
@@ -0,0 +1,276 @@
+package matchers
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+
+ "github.com/onsi/gomega/format"
+)
+
+const (
+ maxScanTokenSize = 1024 * 1024
+)
+
+type HavePatternsMatcher struct {
+ fakeOmegaMatcher
+
+ Elements interface{}
+ missingElements []string
+}
+
+func HavePatterns(elements interface{}) GossMatcher {
+ return &HavePatternsMatcher{
+ Elements: elements,
+ }
+}
+
+func (m *HavePatternsMatcher) Match(actual interface{}) (success bool, err error) {
+ t, ok := m.Elements.([]interface{})
+ if !ok {
+ return false, fmt.Errorf("HavePatterns matcher expects an array of matchers. Got:\n%s", format.Object(m.Elements, 1))
+ }
+ elements := make([]string, len(t))
+ for i, v := range t {
+ switch v := v.(type) {
+ case string:
+ elements[i] = v
+ default:
+ return false, fmt.Errorf("HavePatterns matcher expects patterns to be a string. got: \n%s", format.Object(v, 1))
+ }
+ }
+ notfound, err := sliceToPatterns(elements)
+ if err != nil {
+ return false, err
+ }
+ // short circuit
+ if len(notfound) == 0 {
+ return true, nil
+ }
+ var fh io.Reader
+ switch av := actual.(type) {
+ case io.Reader:
+ fh = av
+ case string:
+ fh = strings.NewReader(av)
+ case []string:
+ fh = strings.NewReader(strings.Join(av, "\n"))
+ default:
+ err = fmt.Errorf("Incorrect type %T", actual)
+
+ }
+ if err != nil {
+ return false, err
+ }
+
+ defer func() {
+ if rc, ok := fh.(io.ReadCloser); ok {
+ rc.Close()
+ }
+ }()
+
+ scanner := bufio.NewScanner(fh)
+ scanner.Buffer(nil, maxScanTokenSize)
+ var found []patternMatcher
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ i := 0
+ for _, pat := range notfound {
+ if pat.Match(line) {
+ // Found it, but wasn't supposed to, don't mark it as found, but remove it from search
+ if !pat.Inverse() {
+ found = append(found, pat)
+ }
+ continue
+ }
+ notfound[i] = pat
+ i++
+ }
+ notfound = notfound[:i]
+ if len(notfound) == 0 {
+ break
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return false, err
+ }
+
+ for _, pat := range notfound {
+ // Didn't find it, but we didn't want to.. so we mark it as found
+ // Empty pattern should match even if input to scanner is empty
+ if pat.Inverse() || pat.Pattern() == "" {
+ found = append(found, pat)
+ }
+ }
+
+ if len(elements) != len(found) {
+ found := patternsToSlice(found)
+ m.missingElements = subtractSlice(elements, found)
+ return false, nil
+ }
+ return true, nil
+}
+
+func (m *HavePatternsMatcher) FailureResult(actual interface{}) MatcherResult {
+ var a interface{}
+ switch actual.(type) {
+ case string, []string:
+ a = actual
+ default:
+ a = fmt.Sprintf("object: %T", actual)
+ }
+ return MatcherResult{
+ Actual: a,
+ Message: "to have patterns",
+ Expected: m.Elements,
+ MissingElements: m.missingElements,
+ }
+}
+
+func (m *HavePatternsMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ a, ok := actual.(string)
+ if !ok {
+ a = fmt.Sprintf("object: %T", actual)
+ }
+ return MatcherResult{
+ Actual: a,
+ Message: "not to have patterns",
+ Expected: m.Elements,
+ }
+}
+
+func appendMissingStrings(message string, missingElements []string) string {
+ if len(missingElements) == 0 {
+ return message
+ }
+ return fmt.Sprintf("%s\nthe missing elements were\n%s", message,
+ format.Object(missingElements, 1))
+}
+
+type patternMatcher interface {
+ Match(string) bool
+ Pattern() string
+ Inverse() bool
+}
+
+type stringPattern struct {
+ pattern string
+ cleanPattern string
+ inverse bool
+}
+
+func newStringPattern(str string) *stringPattern {
+ var inverse bool
+ if strings.HasPrefix(str, "!") {
+ inverse = true
+ }
+ cleanPattern := strings.TrimLeft(str, "\\/!")
+ return &stringPattern{
+ pattern: str,
+ cleanPattern: cleanPattern,
+ inverse: inverse,
+ }
+}
+
+func (s *stringPattern) Match(str string) bool {
+ return strings.Contains(str, s.cleanPattern)
+}
+
+func (s *stringPattern) Pattern() string { return s.pattern }
+func (s *stringPattern) Inverse() bool { return s.inverse }
+
+type regexPattern struct {
+ pattern string
+ re *regexp.Regexp
+ inverse bool
+}
+
+func newRegexPattern(str string) (*regexPattern, error) {
+ var inverse bool
+ cleanStr := str
+ if strings.HasPrefix(str, "!") {
+ inverse = true
+ cleanStr = cleanStr[1:]
+ }
+ trimLeft := []rune{'\\', '/'}
+ for _, r := range trimLeft {
+ if rune(cleanStr[0]) == r {
+ cleanStr = cleanStr[1:]
+ break
+ }
+ }
+ trimRight := []rune{'/'}
+ for _, r := range trimRight {
+ if rune(cleanStr[len(cleanStr)-1]) == r {
+ cleanStr = cleanStr[:len(cleanStr)-1]
+ break
+ }
+ }
+
+ re, err := regexp.Compile(cleanStr)
+
+ return ®exPattern{
+ pattern: str,
+ re: re,
+ inverse: inverse,
+ }, err
+
+}
+
+func (re *regexPattern) Match(str string) bool {
+ return re.re.MatchString(str)
+}
+
+func (re *regexPattern) Pattern() string { return re.pattern }
+func (re *regexPattern) Inverse() bool { return re.inverse }
+
+func sliceToPatterns(slice []string) ([]patternMatcher, error) {
+ var patterns []patternMatcher
+ for _, s := range slice {
+ if (strings.HasPrefix(s, "/") || strings.HasPrefix(s, "!/")) && strings.HasSuffix(s, "/") {
+ pat, err := newRegexPattern(s)
+ if err != nil {
+ return nil, err
+ }
+ patterns = append(patterns, pat)
+ } else {
+ patterns = append(patterns, newStringPattern(s))
+ }
+ }
+ return patterns, nil
+}
+
+func patternsToSlice(patterns []patternMatcher) []string {
+ var slice []string
+ for _, p := range patterns {
+ slice = append(slice, p.Pattern())
+ }
+ return slice
+}
+func subtractSlice(x, y []string) []string {
+ m := make(map[string]bool)
+
+ for _, y := range y {
+ m[y] = true
+ }
+
+ var ret []string
+ for _, x := range x {
+ if m[x] {
+ continue
+ }
+ ret = append(ret, x)
+ }
+
+ return ret
+}
+
+func (matcher *HavePatternsMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["have-patterns"] = matcher.Elements
+ return json.Marshal(j)
+}
diff --git a/matchers/have_prefix_matcher.go b/matchers/have_prefix_matcher.go
new file mode 100644
index 000000000..a9c32eb84
--- /dev/null
+++ b/matchers/have_prefix_matcher.go
@@ -0,0 +1,42 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type HavePrefixMatcher struct {
+ matchers.HavePrefixMatcher
+}
+
+func HavePrefix(prefix string, args ...interface{}) GossMatcher {
+ return &HavePrefixMatcher{
+ matchers.HavePrefixMatcher{
+ Prefix: prefix,
+ Args: args,
+ },
+ }
+}
+
+func (m *HavePrefixMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to have prefix",
+ Expected: m.Prefix,
+ }
+}
+
+func (m *HavePrefixMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to have prefix",
+ Expected: m.Prefix,
+ }
+}
+
+func (m *HavePrefixMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["have-prefix"] = m.Prefix
+ return json.Marshal(j)
+}
diff --git a/matchers/have_suffix_matcher.go b/matchers/have_suffix_matcher.go
new file mode 100644
index 000000000..a4d759296
--- /dev/null
+++ b/matchers/have_suffix_matcher.go
@@ -0,0 +1,42 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type HaveSuffixMatcher struct {
+ matchers.HaveSuffixMatcher
+}
+
+func HaveSuffix(prefix string, args ...interface{}) GossMatcher {
+ return &HaveSuffixMatcher{
+ matchers.HaveSuffixMatcher{
+ Suffix: prefix,
+ Args: args,
+ },
+ }
+}
+
+func (m *HaveSuffixMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to have suffix",
+ Expected: m.Suffix,
+ }
+}
+
+func (m *HaveSuffixMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to have suffix",
+ Expected: m.Suffix,
+ }
+}
+
+func (m *HaveSuffixMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["have-suffix"] = m.Suffix
+ return json.Marshal(j)
+}
diff --git a/matchers/match_regexp_matcher.go b/matchers/match_regexp_matcher.go
new file mode 100644
index 000000000..aba5c463f
--- /dev/null
+++ b/matchers/match_regexp_matcher.go
@@ -0,0 +1,42 @@
+package matchers
+
+import (
+ "encoding/json"
+
+ "github.com/onsi/gomega/matchers"
+)
+
+type MatchRegexpMatcher struct {
+ matchers.MatchRegexpMatcher
+}
+
+func MatchRegexp(regexp string, args ...interface{}) GossMatcher {
+ return &MatchRegexpMatcher{
+ matchers.MatchRegexpMatcher{
+ Regexp: regexp,
+ Args: args,
+ },
+ }
+}
+
+func (m *MatchRegexpMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to match regular expression",
+ Expected: m.Regexp,
+ }
+}
+
+func (m *MatchRegexpMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to match regular expression",
+ Expected: m.Regexp,
+ }
+}
+
+func (m *MatchRegexpMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["match-regexp"] = m.Regexp
+ return json.Marshal(j)
+}
diff --git a/matchers/matchers.go b/matchers/matchers.go
new file mode 100644
index 000000000..93990f564
--- /dev/null
+++ b/matchers/matchers.go
@@ -0,0 +1,48 @@
+package matchers
+
+import (
+ "encoding/json"
+ "reflect"
+ "unsafe"
+
+ "github.com/onsi/gomega/types"
+)
+
+type GossMatcher interface {
+ // This is needed due to oMegaMatcher test in some of the GomegaMatcher logic
+ types.GomegaMatcher
+ //Match(actual interface{}) (success bool, err error)
+ FailureResult(actual interface{}) MatcherResult
+ NegatedFailureResult(actual interface{}) MatcherResult
+ // This doesn't seem to make a difference, maybe not needed
+ json.Marshaler
+}
+
+type MatcherResult struct {
+ Actual interface{}
+ Message string
+ Expected interface{}
+ MissingElements interface{}
+ ExtraElements interface{}
+ TransformerChain []Transformer
+ UntransformedValue interface{}
+}
+
+func getUnexported(i interface{}, field string) interface{} {
+ rs := reflect.ValueOf(i).Elem()
+ rf := rs.FieldByName(field)
+ rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
+ return rf.Interface()
+}
+
+type fakeOmegaMatcher struct{}
+
+// FailureMessage is a stub to honor omegaMatcher interface
+func (m *fakeOmegaMatcher) FailureMessage(_ interface{}) (message string) {
+ return ""
+}
+
+// NegatedFailureMessage is a stub to honor omegaMatcher interface
+func (m *fakeOmegaMatcher) NegatedFailureMessage(_ interface{}) (message string) {
+ return ""
+}
diff --git a/matchers/not.go b/matchers/not.go
new file mode 100644
index 000000000..c27373d59
--- /dev/null
+++ b/matchers/not.go
@@ -0,0 +1,36 @@
+package matchers
+
+import (
+ "encoding/json"
+)
+
+type NotMatcher struct {
+ fakeOmegaMatcher
+ Matcher GossMatcher
+}
+
+func Not(matcher GossMatcher) GossMatcher {
+ return &NotMatcher{Matcher: matcher}
+}
+
+func (m *NotMatcher) Match(actual interface{}) (bool, error) {
+ success, err := m.Matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ return !success, nil
+}
+
+func (m *NotMatcher) FailureResult(actual interface{}) MatcherResult {
+ return m.Matcher.NegatedFailureResult(actual)
+}
+
+func (m *NotMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ return m.Matcher.FailureResult(actual)
+}
+
+func (m *NotMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["not"] = m.Matcher
+ return json.Marshal(j)
+}
diff --git a/matchers/or.go b/matchers/or.go
new file mode 100644
index 000000000..f34dc4e3a
--- /dev/null
+++ b/matchers/or.go
@@ -0,0 +1,52 @@
+package matchers
+
+import (
+ "encoding/json"
+)
+
+type OrMatcher struct {
+ fakeOmegaMatcher
+
+ Matchers []GossMatcher
+
+ // state
+ firstSuccessfulMatcher GossMatcher
+}
+
+func Or(ms ...GossMatcher) GossMatcher {
+ return &OrMatcher{Matchers: ms}
+}
+
+func (m *OrMatcher) Match(actual interface{}) (success bool, err error) {
+ m.firstSuccessfulMatcher = nil
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ if success {
+ m.firstSuccessfulMatcher = matcher
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (m *OrMatcher) FailureResult(actual interface{}) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to satisfy at least one of these matchers",
+ Expected: m.Matchers,
+ }
+}
+
+func (m *OrMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ firstSuccessfulMatcher := getUnexported(m, "firstSuccessfulMatcher")
+ return firstSuccessfulMatcher.(GossMatcher).NegatedFailureResult(actual)
+}
+
+func (m *OrMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]interface{})
+ j["or"] = m.Matchers
+ return json.Marshal(j)
+}
diff --git a/matchers/semver_constraint.go b/matchers/semver_constraint.go
index b69039dbe..f95d934a4 100644
--- a/matchers/semver_constraint.go
+++ b/matchers/semver_constraint.go
@@ -1,28 +1,30 @@
package matchers
import (
+ "bytes"
+ "encoding/json"
"fmt"
"reflect"
"github.com/blang/semver/v4"
"github.com/onsi/gomega/format"
- "github.com/onsi/gomega/types"
)
-func BeSemverConstraint(constraint any) types.GomegaMatcher {
- return &BeSemverConstraintMatcher{
- Constraint: constraint,
- }
-}
-
type BeSemverConstraintMatcher struct {
+ fakeOmegaMatcher
+
Constraint any
}
-func (matcher *BeSemverConstraintMatcher) Match(actual any) (success bool, err error) {
- constraint, ok := toConstraint(matcher.Constraint)
+func BeSemverConstraint(constraint any) GossMatcher {
+ return &BeSemverConstraintMatcher{
+ Constraint: constraint,
+ }
+}
+func (m *BeSemverConstraintMatcher) Match(actual any) (success bool, err error) {
+ constraint, ok := toConstraint(m.Constraint)
if !ok {
- return false, fmt.Errorf("Expected a valid semver constraint. Got:\n%s", format.Object(matcher.Constraint, 1))
+ return false, fmt.Errorf("Expected a valid semver constraint. Got:\n%s", format.Object(m.Constraint, 1))
}
actualSlice, ok := toVersions(actual)
@@ -39,12 +41,20 @@ func (matcher *BeSemverConstraintMatcher) Match(actual any) (success bool, err e
return true, nil
}
-func (matcher *BeSemverConstraintMatcher) FailureMessage(actual any) (message string) {
- return format.Message(actual, fmt.Sprintf("to be %s", matcher.Constraint))
+func (m *BeSemverConstraintMatcher) FailureResult(actual any) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "to satisfy semver constraint",
+ Expected: m.Constraint,
+ }
}
-func (matcher *BeSemverConstraintMatcher) NegatedFailureMessage(actual any) (message string) {
- return format.Message(actual, fmt.Sprintf("not to be %s", matcher.Constraint))
+func (m *BeSemverConstraintMatcher) NegatedFailureResult(actual any) MatcherResult {
+ return MatcherResult{
+ Actual: actual,
+ Message: "not to satisfy semver constraint",
+ Expected: m.Constraint,
+ }
}
func toConstraint(in any) (semver.Range, bool) {
@@ -102,3 +112,17 @@ func toVersions(in any) ([]*semver.Version, bool) {
return out, len(out) > 0
}
+
+func (m *BeSemverConstraintMatcher) MarshalJSON() ([]byte, error) {
+ j := make(map[string]any)
+ j["semver-constraint"] = m.Constraint
+ buffer := &bytes.Buffer{}
+ encoder := json.NewEncoder(buffer)
+ encoder.SetEscapeHTML(false)
+ err := encoder.Encode(j)
+ if err != nil {
+ return nil, nil
+ }
+ b := buffer.Bytes()
+ return b, nil
+}
diff --git a/matchers/semver_constraint_test.go b/matchers/semver_constraint_test.go
index 4dfaef4d7..db5e8e1d3 100644
--- a/matchers/semver_constraint_test.go
+++ b/matchers/semver_constraint_test.go
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/blang/semver/v4"
- "github.com/onsi/gomega/types"
)
func TestBeSemverConstraint(t *testing.T) {
@@ -17,7 +16,7 @@ func TestBeSemverConstraint(t *testing.T) {
tests := []struct {
name string
args args
- want types.GomegaMatcher
+ want GossMatcher
}{
{
name: "sanity",
@@ -42,22 +41,20 @@ func TestBeSemverConstraintMatcher_FailureMessage(t *testing.T) {
actual any
}
tests := []struct {
- name string
- fields fields
- args args
- wantMessage string
+ name string
+ fields fields
+ args args
+ wantResult MatcherResult
}{
{
- name: "string",
- fields: fields{Constraint: "> 1.1.0"},
- args: args{actual: "1.0.0"},
- wantMessage: "Expected\n : 1.0.0\nto be > 1.1.0",
- },
- {
- name: "slice_string",
- fields: fields{Constraint: "> 1.1.0"},
- args: args{actual: []string{"1.0.0"}},
- wantMessage: "Expected\n <[]string | len:1, cap:1>: [\"1.0.0\"]\nto be > 1.1.0",
+ name: "string",
+ fields: fields{Constraint: "> 1.1.0"},
+ args: args{actual: "1.0.0"},
+ wantResult: MatcherResult{
+ Actual: "1.0.0",
+ Message: "to satisfy semver constraint",
+ Expected: "> 1.1.0",
+ },
},
}
for _, tt := range tests {
@@ -65,8 +62,8 @@ func TestBeSemverConstraintMatcher_FailureMessage(t *testing.T) {
matcher := &BeSemverConstraintMatcher{
Constraint: tt.fields.Constraint,
}
- gotMessage := matcher.FailureMessage(tt.args.actual)
- assert.Equal(t, tt.wantMessage, gotMessage)
+ gotResult := matcher.FailureResult(tt.args.actual)
+ assert.Equal(t, tt.wantResult, gotResult)
})
}
}
@@ -171,22 +168,20 @@ func TestBeSemverConstraintMatcher_NegatedFailureMessage(t *testing.T) {
actual any
}
tests := []struct {
- name string
- fields fields
- args args
- wantMessage string
+ name string
+ fields fields
+ args args
+ wantResult MatcherResult
}{
{
- name: "string",
- fields: fields{Constraint: "> 1.1.0"},
- args: args{actual: "1.0.0"},
- wantMessage: "Expected\n : 1.0.0\nnot to be > 1.1.0",
- },
- {
- name: "slice_string",
- fields: fields{Constraint: "> 1.1.0"},
- args: args{actual: []string{"1.0.0"}},
- wantMessage: "Expected\n <[]string | len:1, cap:1>: [\"1.0.0\"]\nnot to be > 1.1.0",
+ name: "string",
+ fields: fields{Constraint: "> 1.1.0"},
+ args: args{actual: "1.0.0"},
+ wantResult: MatcherResult{
+ Actual: "1.0.0",
+ Message: "not to satisfy semver constraint",
+ Expected: "> 1.1.0",
+ },
},
}
for _, tt := range tests {
@@ -195,8 +190,8 @@ func TestBeSemverConstraintMatcher_NegatedFailureMessage(t *testing.T) {
Constraint: tt.fields.Constraint,
}
- gotMessage := matcher.NegatedFailureMessage(tt.args.actual)
- assert.Equal(t, tt.wantMessage, gotMessage)
+ gotResult := matcher.NegatedFailureResult(tt.args.actual)
+ assert.Equal(t, tt.wantResult, gotResult)
})
}
}
diff --git a/matchers/type_conversion.go b/matchers/type_conversion.go
new file mode 100644
index 000000000..0d425bb4f
--- /dev/null
+++ b/matchers/type_conversion.go
@@ -0,0 +1,144 @@
+package matchers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "strconv"
+ "strings"
+
+ "github.com/onsi/gomega/format"
+ "github.com/tidwall/gjson"
+)
+
+type Transformer interface {
+ Transform(interface{}) (interface{}, error)
+}
+
+type ToNumeric struct{}
+
+func (t ToNumeric) Transform(e interface{}) (interface{}, error) {
+ switch v := e.(type) {
+ case float64, int:
+ return v, nil
+ case string:
+ return strconv.ParseFloat(strings.TrimSpace(v), 64)
+ case []string:
+ i, err := ToString{}.Transform(v)
+ if err != nil {
+ return 0, err
+ }
+ s := i.(string)
+ return strconv.ParseFloat(strings.TrimSpace(s), 64)
+ default:
+ return 0, fmt.Errorf("Expected numeric, Got:%s", format.Object(e, 1))
+
+ }
+}
+func (t ToNumeric) MarshalJSON() ([]byte, error) {
+ j := map[string]interface{}{
+ "to-numeric": map[string]string{},
+ }
+ return json.Marshal(j)
+}
+
+type ToString struct{}
+
+func (t ToString) Transform(e interface{}) (interface{}, error) {
+ switch v := e.(type) {
+ case []interface{}:
+ vs := make([]string, len(v))
+ for i, v := range v {
+ vs[i] = fmt.Sprintf("%v", v)
+ }
+ return strings.Join(vs, "\n"), nil
+ case []string:
+ return strings.Join(v, "\n"), nil
+ default:
+ return fmt.Sprintf("%v", v), nil
+ }
+}
+
+func (t ToString) MarshalJSON() ([]byte, error) {
+ j := map[string]interface{}{
+ "to-string": map[string]string{},
+ }
+ return json.Marshal(j)
+}
+
+type ToArray struct{}
+
+func (t ToArray) Transform(i interface{}) (interface{}, error) {
+ switch v := i.(type) {
+ case string:
+ return strings.Split(v, "\n"), nil
+ default:
+ return i, nil
+ }
+}
+func (matcher ToArray) MarshalJSON() ([]byte, error) {
+ j := map[string]interface{}{
+ "to-array": map[string]string{},
+ }
+ return json.Marshal(j)
+}
+
+//type ReaderToStrings struct{}
+//
+//func (t ReaderToStrings) Transform(i interface{}) (interface{}, error) {
+// r, ok := i.(io.Reader)
+// if !ok {
+// return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1))
+// }
+// var lines []string
+// i, err := ReaderToString{}.Transform(r)
+// if err != nil {
+// return lines, err
+// }
+// s := i.(string)
+// return strings.Split(s, "\n"), nil
+//}
+
+type ReaderToString struct{}
+
+func (t ReaderToString) Transform(i interface{}) (interface{}, error) {
+ r, ok := i.(io.Reader)
+ if !ok {
+ return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1))
+ }
+
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+type Gjson struct {
+ Path string
+}
+
+func (g Gjson) Transform(i interface{}) (interface{}, error) {
+ s, ok := i.(string)
+ if !ok {
+ return nil, fmt.Errorf("Expected string, Got:%s", format.Object(i, 1))
+ }
+ if !gjson.Valid(s) {
+ return nil, fmt.Errorf("Invalid json")
+ }
+ r := gjson.Get(s, g.Path)
+ if !r.Exists() {
+ return nil, fmt.Errorf("Path not found: %s", g.Path)
+ }
+
+ return r.Value(), nil
+}
+func (g Gjson) MarshalJSON() ([]byte, error) {
+ j := map[string]interface{}{
+ "gjson": map[string]string{
+ "Path": g.Path,
+ },
+ }
+ return json.Marshal(j)
+}
diff --git a/matchers/with_safe_transform.go b/matchers/with_safe_transform.go
new file mode 100644
index 000000000..0629c1417
--- /dev/null
+++ b/matchers/with_safe_transform.go
@@ -0,0 +1,82 @@
+package matchers
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+)
+
+type WithSafeTransformMatcher struct {
+ fakeOmegaMatcher
+
+ // input
+ Transform Transformer // must be a function of one parameter that returns one value
+ Matcher GossMatcher
+
+ // state
+ transformedValue interface{}
+ wasTransformed bool
+}
+
+func WithSafeTransform(transform Transformer, matcher GossMatcher) GossMatcher {
+
+ return &WithSafeTransformMatcher{
+ Transform: transform,
+ Matcher: matcher,
+ }
+}
+
+func (m *WithSafeTransformMatcher) Match(actual interface{}) (bool, error) {
+ var err error
+ //log.Printf("%#v: input: %v", m.Transform, actual)
+ m.transformedValue, err = m.Transform.Transform(actual)
+ if !reflect.DeepEqual(actual, m.transformedValue) {
+ m.wasTransformed = true
+ }
+ if err != nil {
+ return false, fmt.Errorf("%#v: %s", m.Transform, err)
+ }
+ //log.Printf("%#v: output: %v", m.Transform, m.transformedValue)
+ return m.Matcher.Match(m.transformedValue)
+}
+
+func (m *WithSafeTransformMatcher) FailureResult(actual interface{}) MatcherResult {
+ tchain, matcher, tvalue := m.getTransformerChainAndMatcher()
+ result := matcher.FailureResult(tvalue)
+ result.TransformerChain = tchain
+ result.UntransformedValue = actual
+ return result
+}
+func (m *WithSafeTransformMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
+ tchain, matcher, tvalue := m.getTransformerChainAndMatcher()
+ result := matcher.NegatedFailureResult(tvalue)
+ result.TransformerChain = tchain
+ result.UntransformedValue = actual
+ return result
+}
+
+func (m *WithSafeTransformMatcher) getTransformerChainAndMatcher() (tchain []Transformer, matcher GossMatcher, tvalue interface{}) {
+ matcher = m
+ tvalue = m.transformedValue
+L:
+ for {
+ switch v := matcher.(type) {
+ case *WithSafeTransformMatcher:
+ matcher = v.Matcher
+ tvalue = v.transformedValue
+ if v.wasTransformed {
+ tchain = append(tchain, v.Transform)
+ }
+ default:
+ break L
+
+ }
+ }
+ return tchain, matcher, tvalue
+
+}
+
+func (m *WithSafeTransformMatcher) MarshalJSON() ([]byte, error) {
+ _, matcher, _ := m.getTransformerChainAndMatcher()
+ return json.Marshal(matcher)
+}
diff --git a/outputs/documentation.go b/outputs/documentation.go
index 758e3b001..7186ff33a 100644
--- a/outputs/documentation.go
+++ b/outputs/documentation.go
@@ -12,12 +12,20 @@ import (
type Documentation struct{}
func (r Documentation) ValidOptions() []*formatOption {
- return []*formatOption{}
+ return []*formatOption{
+ {name: foSort},
+ }
}
func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
+ var startTime time.Time
+ var endTime time.Time
testCount := 0
var failedOrSkipped [][]resource.TestResult
var skipped, failed int
@@ -29,15 +37,21 @@ func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult,
fmt.Fprint(w, header)
}
for _, testResult := range resultGroup {
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
switch testResult.Result {
case resource.SUCCESS:
- fmt.Fprintln(w, humanizeResult(testResult))
+ fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
case resource.SKIP:
- fmt.Fprintln(w, humanizeResult(testResult))
+ fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
skipped++
case resource.FAIL:
- fmt.Fprintln(w, humanizeResult(testResult))
+ fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
failed++
}
@@ -49,9 +63,9 @@ func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult,
}
fmt.Fprint(w, "\n\n")
- fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped))
+ fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw))
- fmt.Fprint(w, summary(startTime, testCount, failed, skipped))
+ fmt.Fprint(w, summary(startTime, endTime, testCount, failed, skipped))
if failed > 0 {
return 1
}
diff --git a/outputs/json.go b/outputs/json.go
index 605863775..88354b438 100644
--- a/outputs/json.go
+++ b/outputs/json.go
@@ -17,15 +17,22 @@ type Json struct{}
func (r Json) ValidOptions() []*formatOption {
return []*formatOption{
{name: foPretty},
+ {name: foSort},
}
}
func (r Json) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
var pretty bool
pretty = util.IsValueInList(foPretty, outConfig.FormatOptions)
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
+
+ var startTime time.Time
+ var endTime time.Time
color.NoColor = true
testCount := 0
failed := 0
@@ -33,7 +40,13 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult,
var resultsOut []map[string]any
for resultGroup := range results {
for _, testResult := range resultGroup {
- if !testResult.Successful {
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
+ if testResult.Result == resource.FAIL {
failed++
logTrace("WARN", "FAIL", testResult, true)
} else {
@@ -43,7 +56,8 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult,
skipped++
}
m := struct2map(testResult)
- m["summary-line"] = humanizeResult(testResult)
+ m["summary-line"] = humanizeResult(testResult, false, includeRaw)
+ m["summary-line-compact"] = humanizeResult(testResult, true, includeRaw)
m["duration"] = int64(m["duration"].(float64))
resultsOut = append(resultsOut, m)
testCount++
@@ -51,7 +65,7 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult,
}
summary := make(map[string]any)
- duration := time.Since(startTime)
+ duration := endTime.Sub(startTime)
summary["test-count"] = testCount
summary["failed-count"] = failed
summary["skipped-count"] = skipped
diff --git a/outputs/json_oneline.go b/outputs/json_oneline.go
deleted file mode 100644
index 35dee2267..000000000
--- a/outputs/json_oneline.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package outputs
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "time"
-
- "github.com/fatih/color"
- "github.com/goss-org/goss/resource"
- "github.com/goss-org/goss/util"
-)
-
-type JsonOneline struct{}
-
-func (r JsonOneline) ValidOptions() []*formatOption {
- return []*formatOption{}
-}
-
-func (r JsonOneline) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
-
- color.NoColor = true
- testCount := 0
- failed := 0
- var resultsOut []map[string]any
- for resultGroup := range results {
- for _, testResult := range resultGroup {
- if !testResult.Successful {
- failed++
- }
- m := struct2map(testResult)
- m["summary-line"] = humanizeResult(testResult)
- m["duration"] = int64(m["duration"].(float64))
- resultsOut = append(resultsOut, m)
- testCount++
- }
- }
-
- summary := make(map[string]any)
- duration := time.Since(startTime)
- summary["test-count"] = testCount
- summary["failed-count"] = failed
- summary["total-duration"] = duration
- summary["summary-line"] = fmt.Sprintf("Count: %d, Failed: %d, Duration: %.3fs", testCount, failed, duration.Seconds())
-
- out := make(map[string]any)
- out["results"] = resultsOut
- out["summary"] = summary
-
- j, _ := json.Marshal(out)
- fmt.Fprintln(w, string(j))
-
- if failed > 0 {
- return 1
- }
-
- return 0
-}
diff --git a/outputs/junit.go b/outputs/junit.go
index dd0d8a82b..dc547d2be 100644
--- a/outputs/junit.go
+++ b/outputs/junit.go
@@ -16,11 +16,17 @@ import (
type JUnit struct{}
func (r JUnit) ValidOptions() []*formatOption {
- return []*formatOption{}
+ return []*formatOption{
+ {name: foSort},
+ }
}
func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
color.NoColor = true
var testCount, failed, skipped int
@@ -31,8 +37,16 @@ func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult,
var summary map[int]string
summary = make(map[int]string)
+ var startTime time.Time
+ var endTime time.Time
for resultGroup := range results {
for _, testResult := range resultGroup {
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
m := struct2map(testResult)
duration := strconv.FormatFloat(m["duration"].(float64)/1000/1000/1000, 'f', 3, 64)
summary[testCount] = "\n"
if testResult.Result == resource.FAIL {
summary[testCount] += "" +
- escapeString(humanizeResult2(testResult)) +
+ escapeString(humanizeResult(testResult, true, includeRaw)) +
"\n"
summary[testCount] += "" +
- escapeString(humanizeResult2(testResult)) +
+ escapeString(humanizeResult(testResult, true, includeRaw)) +
"\n\n"
failed++
@@ -55,14 +69,14 @@ func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult,
skipped++
}
summary[testCount] += "" +
- escapeString(humanizeResult2(testResult)) +
+ escapeString(humanizeResult(testResult, true, includeRaw)) +
"\n\n"
}
testCount++
}
}
- duration := time.Since(startTime)
+ duration := endTime.Sub(startTime)
fmt.Fprintln(w, "")
fmt.Fprintf(w, "\n",
diff --git a/outputs/nagios.go b/outputs/nagios.go
index e5fe6bf36..d91776dba 100644
--- a/outputs/nagios.go
+++ b/outputs/nagios.go
@@ -20,23 +20,32 @@ func (r Nagios) ValidOptions() []*formatOption {
}
func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
var testCount, failed, skipped int
var perfdata, verbose bool
perfdata = util.IsValueInList(foPerfData, outConfig.FormatOptions)
verbose = util.IsValueInList(foVerbose, outConfig.FormatOptions)
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+ var startTime time.Time
+ var endTime time.Time
var summary map[int]string
summary = make(map[int]string)
for resultGroup := range results {
for _, testResult := range resultGroup {
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
switch testResult.Result {
case resource.FAIL:
if util.IsValueInList(foVerbose, outConfig.FormatOptions) {
- summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult2(testResult) + "\n"
+ summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
}
failed++
case resource.SKIP:
@@ -46,7 +55,7 @@ func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult,
}
}
- duration := time.Since(startTime)
+ duration := endTime.Sub(startTime)
if failed > 0 {
fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds())
if perfdata {
diff --git a/outputs/outputs.go b/outputs/outputs.go
index c3c11595c..c4e8d2ece 100644
--- a/outputs/outputs.go
+++ b/outputs/outputs.go
@@ -1,16 +1,22 @@
package outputs
import (
+ "bytes"
+ "encoding/json"
"fmt"
"io"
+ "reflect"
+ "regexp"
"sort"
"strings"
"sync"
"time"
+ "unicode"
"github.com/fatih/color"
"github.com/goss-org/goss/resource"
"github.com/goss-org/goss/util"
+ "github.com/icza/dyno"
)
type formatOption struct {
@@ -18,7 +24,7 @@ type formatOption struct {
}
type Outputer interface {
- Output(io.Writer, <-chan []resource.TestResult, time.Time, util.OutputConfig) int
+ Output(io.Writer, <-chan []resource.TestResult, util.OutputConfig) int
ValidOptions() []*formatOption
}
@@ -26,7 +32,6 @@ var (
outputersMu sync.Mutex
outputers = map[string]Outputer{
"documentation": &Documentation{},
- "json_oneline": &JsonOneline{},
"json": &Json{},
"junit": &JUnit{},
"nagios": &Nagios{},
@@ -36,68 +41,110 @@ var (
"tap": &Tap{},
"silent": &Silent{},
}
- foPerfData = "perfdata"
- foVerbose = "verbose"
- foPretty = "pretty"
+ foPerfData = "perfdata"
+ foVerbose = "verbose"
+ foPretty = "pretty"
+ foExcludeRaw = "exclude_raw"
+ foSort = "sort"
)
var green = color.New(color.FgGreen).SprintfFunc()
var red = color.New(color.FgRed).SprintfFunc()
var yellow = color.New(color.FgYellow).SprintfFunc()
+var multiple_space = regexp.MustCompile(`\s+`)
-func humanizeResult(r resource.TestResult) string {
- if r.Err != nil {
- return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err)
+func humanizeResult(r resource.TestResult, compact bool, includeRaw bool) string {
+ sep := "\n"
+ if compact {
+ sep = " "
}
switch r.Result {
case resource.SUCCESS:
- return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected)
+ return green("%s: %s: %s: %s: %s", r.ResourceType, r.ResourceId, r.Property, r.MatcherResult.Message, prettyPrint(r.MatcherResult.Expected, false))
+ case resource.FAIL:
+ matcherResult := prettyPrintTestResult(r, compact, includeRaw)
+ return red("%s: %s: %s:%s%s", r.ResourceType, r.ResourceId, r.Property, sep, matcherResult)
case resource.SKIP:
return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property)
- case resource.FAIL:
- if r.Human != "" {
- return red("%s: %s: %s:\n%s", r.ResourceType, r.ResourceId, r.Property, r.Human)
- }
- return humanizeResult2(r)
default:
panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result))
}
}
-func humanizeResult2(r resource.TestResult) string {
- if r.Err != nil {
- return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err)
+func prettyPrintTestResult(t resource.TestResult, compact bool, includeRaw bool) string {
+ sep := "\n"
+ if compact {
+ sep = " "
}
-
- switch r.Result {
- case resource.SUCCESS:
- switch r.TestType {
- case resource.Value:
- return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected)
- case resource.Values:
- return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", "))
- case resource.Contains:
- return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", "))
- default:
- return red("Unexpected type %d", r.TestType)
+ m := t.MatcherResult
+ var ss []string
+ //var s string
+ if t.Err != nil {
+ e := fmt.Sprint(t.Err)
+ if compact {
+ e = multiple_space.ReplaceAllString(e, " ")
+ } else {
+ e = indentLines(e)
}
- case resource.FAIL:
- switch r.TestType {
- case resource.Value:
- return red("%s: %s: %s: doesn't match, expect: %s found: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected, r.Found)
- case resource.Values:
- return red("%s: %s: %s: expectations not found [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", "))
- case resource.Contains:
- return red("%s: %s: %s: patterns not found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", "))
- default:
- return red("Unexpected type %d", r.TestType)
+ ss = append(ss, "Error")
+ ss = append(ss, e)
+ } else {
+ ss = append(ss, "Expected")
+ ss = append(ss, prettyPrint(m.Actual, !compact))
+ ss = append(ss, m.Message)
+ ss = append(ss, prettyPrint(m.Expected, !compact))
+ }
+
+ if reflect.ValueOf(m.MissingElements).IsValid() && !reflect.ValueOf(m.MissingElements).IsNil() {
+ ss = append(ss, "the missing elements were")
+ ss = append(ss, prettyPrint(m.MissingElements, !compact))
+ }
+ if reflect.ValueOf(m.ExtraElements).IsValid() && !reflect.ValueOf(m.ExtraElements).IsNil() {
+ ss = append(ss, "the extra elements were")
+ ss = append(ss, prettyPrint(m.ExtraElements, !compact))
+ }
+ if len(m.TransformerChain) != 0 {
+ ss = append(ss, "the transform chain was")
+ ss = append(ss, prettyPrint(m.TransformerChain, !compact))
+ if includeRaw {
+ ss = append(ss, "the raw value was")
+ ss = append(ss, prettyPrint(m.UntransformedValue, !compact))
}
- case resource.SKIP:
- return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property)
- default:
- panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result))
}
+ return strings.Join(ss, sep)
+}
+
+func prettyPrint(i interface{}, indent bool) string {
+ // JSON doesn't like non-string keys
+ i = dyno.ConvertMapI2MapS(i)
+ buffer := &bytes.Buffer{}
+ encoder := json.NewEncoder(buffer)
+ encoder.SetEscapeHTML(false)
+ var b []byte
+ err := encoder.Encode(i)
+ if err == nil {
+ b = buffer.Bytes()
+ } else {
+ // FIXME: Is this the right thing to do?
+ b = []byte(err.Error())
+ }
+ b = bytes.TrimRightFunc(b, unicode.IsSpace)
+ if indent {
+ return indentLines(string(b))
+ } else {
+ return string(b)
+ }
+}
+
+// indents a block of text with an indent string
+func indentLines(text string) string {
+ indent := " "
+ result := ""
+ for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") {
+ result += indent + j + "\n"
+ }
+ return result[:len(result)-1]
}
func RegisterOutputer(name string, outputer Outputer) {
@@ -161,24 +208,6 @@ func GetOutputer(name string) (Outputer, error) {
return outputers[name], nil
}
-func subtractSlice(x, y []string) []string {
- m := make(map[string]bool)
-
- for _, y := range y {
- m[y] = true
- }
-
- var ret []string
- for _, x := range x {
- if m[x] {
- continue
- }
- ret = append(ret, x)
- }
-
- return ret
-}
-
func header(t resource.TestResult) string {
var out string
if t.Title != "" {
@@ -199,9 +228,9 @@ func header(t resource.TestResult) string {
return out
}
-func summary(startTime time.Time, count, failed, skipped int) string {
+func summary(startTime, endTime time.Time, count, failed, skipped int) string {
var s string
- s += fmt.Sprintf("Total Duration: %.3fs\n", time.Since(startTime).Seconds())
+ s += fmt.Sprintf("Total Duration: %.3fs\n", endTime.Sub(startTime).Seconds())
f := green
if failed > 0 {
f = red
@@ -210,10 +239,13 @@ func summary(startTime time.Time, count, failed, skipped int) string {
return s
}
-func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string {
+func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult, includeRaw bool) string {
var s string
if len(failedOrSkipped) > 0 {
s += fmt.Sprint("Failures/Skipped:\n\n")
+ sort.Slice(failedOrSkipped, func(i, j int) bool {
+ return failedOrSkipped[i][0].SortKey() < failedOrSkipped[j][0].SortKey()
+ })
for _, failedGroup := range failedOrSkipped {
first := failedGroup[0]
header := header(first)
@@ -221,10 +253,35 @@ func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string {
s += fmt.Sprint(header)
}
for _, testResult := range failedGroup {
- s += fmt.Sprintln(humanizeResult(testResult))
+ s += fmt.Sprintln(humanizeResult(testResult, false, includeRaw))
}
s += fmt.Sprint("\n")
}
}
return s
}
+
+func getResults(tr <-chan []resource.TestResult, doSort bool) <-chan []resource.TestResult {
+ if !doSort {
+ return tr
+ }
+ str := make([][]resource.TestResult, 0)
+ for i := range tr {
+ str = append(str, i)
+ }
+
+ sort.Slice(str, func(i, j int) bool {
+ return str[i][0].SortKey() < str[j][0].SortKey()
+ })
+
+ c := make(chan []resource.TestResult)
+ go func(c chan []resource.TestResult) {
+ defer close(c)
+
+ for _, i := range str {
+ c <- i
+ }
+ }(c)
+
+ return c
+}
diff --git a/outputs/outputs_test.go b/outputs/outputs_test.go
index b5171579e..141722ed5 100644
--- a/outputs/outputs_test.go
+++ b/outputs/outputs_test.go
@@ -41,13 +41,12 @@ func TestOutputFormatOptions(t *testing.T) {
assert.Contains(t, list, foPerfData)
assert.Contains(t, list, foPretty)
assert.Contains(t, list, foVerbose)
- assert.Len(t, list, 3)
+ assert.Len(t, list, 4)
}
func TestOptionsRegistration(t *testing.T) {
registeredOutputs := Outputers()
assert.Contains(t, registeredOutputs, "documentation")
- assert.Contains(t, registeredOutputs, "json_oneline")
assert.Contains(t, registeredOutputs, "json")
assert.Contains(t, registeredOutputs, "junit")
assert.Contains(t, registeredOutputs, "nagios")
diff --git a/outputs/prometheus.go b/outputs/prometheus.go
index be9c51b9d..21be45ec3 100644
--- a/outputs/prometheus.go
+++ b/outputs/prometheus.go
@@ -73,10 +73,14 @@ func (r Prometheus) ValidOptions() []*formatOption {
// Output converts the results into the prometheus text-format.
func (r Prometheus) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
overallOutcome := resource.OutcomeUnknown
+ var startTime time.Time
for resultGroup := range results {
for _, tr := range resultGroup {
+ if startTime.IsZero() || tr.StartTime.Before(startTime) {
+ startTime = tr.StartTime
+ }
resType := strings.ToLower(tr.ResourceType)
outcome := tr.ToOutcome()
testOutcomes.WithLabelValues(resType, outcome).Inc()
diff --git a/outputs/prometheus_test.go b/outputs/prometheus_test.go
index 82a785dc7..3a4689c82 100644
--- a/outputs/prometheus_test.go
+++ b/outputs/prometheus_test.go
@@ -37,7 +37,7 @@ func TestPrometheusOutput(t *testing.T) {
},
}
- exitCode := outputer.Output(buf, makeResults(injectedResults...), time.Now().Add(-1*time.Minute), util.OutputConfig{})
+ exitCode := outputer.Output(buf, makeResults(injectedResults...), util.OutputConfig{})
assert.Equal(t, 0, exitCode)
output := buf.String()
diff --git a/outputs/rspecish.go b/outputs/rspecish.go
index 84f7e20c0..8c3402602 100644
--- a/outputs/rspecish.go
+++ b/outputs/rspecish.go
@@ -18,14 +18,28 @@ func (r Rspecish) ValidOptions() []*formatOption {
}
func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
+
+ var startTime time.Time
+ var endTime time.Time
testCount := 0
var failedOrSkipped [][]resource.TestResult
var skipped, failed int
for resultGroup := range results {
failedOrSkippedGroup := []resource.TestResult{}
for _, testResult := range resultGroup {
+ // Calculates the start and end times based on the start of the first test
+ // and end of the last test, this allows the time/duration to be stable
+ // FIXME: move this to shared code
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
switch testResult.Result {
case resource.SUCCESS:
logTrace("TRACE", "SUCCESS", testResult, false)
@@ -49,9 +63,11 @@ func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult,
}
fmt.Fprint(w, "\n\n")
- fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped))
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+
+ fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw))
- outstr := summary(startTime, testCount, failed, skipped)
+ outstr := summary(startTime, endTime, testCount, failed, skipped)
fmt.Fprint(w, outstr)
resstr := strings.ReplaceAll(outstr, "\n", " ")
if failed > 0 {
diff --git a/outputs/silent.go b/outputs/silent.go
index 746a4b063..eaaf94c6b 100644
--- a/outputs/silent.go
+++ b/outputs/silent.go
@@ -2,7 +2,6 @@ package outputs
import (
"io"
- "time"
"github.com/goss-org/goss/resource"
"github.com/goss-org/goss/util"
@@ -15,7 +14,7 @@ func (r Silent) ValidOptions() []*formatOption {
}
func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
var failed int
for resultGroup := range results {
diff --git a/outputs/structured.go b/outputs/structured.go
index 146b89f05..ce988e68a 100644
--- a/outputs/structured.go
+++ b/outputs/structured.go
@@ -16,13 +16,15 @@ type Structured struct{}
func (r Structured) ValidOptions() []*formatOption {
return []*formatOption{
{name: foPretty},
+ {name: foSort},
}
}
// StructuredTestResult is an individual test result with additional human friendly summary
type StructuredTestResult struct {
resource.TestResult
- SummaryLine string `json:"summary-line"`
+ SummaryLine string `json:"summary-line"`
+ SummaryLineCompact string `json:"summary-line-compact"`
}
// StructureTestSummary holds summary information about a test run
@@ -45,20 +47,34 @@ func (s *StructureTestSummary) String() string {
}
// Output processes output from tests into StructuredOutput written to w as a string
-func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, outConfig util.OutputConfig) (exitCode int) {
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
+
result := &StructuredOutput{
Results: []StructuredTestResult{},
Summary: StructureTestSummary{},
}
+ var startTime time.Time
+ var endTime time.Time
for resultGroup := range results {
for _, testResult := range resultGroup {
+ if startTime.IsZero() || testResult.StartTime.Before(startTime) {
+ startTime = testResult.StartTime
+ }
+ if endTime.IsZero() || testResult.EndTime.After(endTime) {
+ endTime = testResult.EndTime
+ }
r := StructuredTestResult{
- TestResult: testResult,
- SummaryLine: humanizeResult(testResult),
+ TestResult: testResult,
+ SummaryLine: humanizeResult(testResult, false, includeRaw),
+ SummaryLineCompact: humanizeResult(testResult, true, includeRaw),
}
- if !testResult.Successful {
+ if testResult.Result == resource.FAIL {
result.Summary.Failed++
}
@@ -68,7 +84,7 @@ func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, st
}
}
- result.Summary.TotalDuration = time.Since(startTime)
+ result.Summary.TotalDuration = endTime.Sub(startTime)
result.SummaryLine = result.Summary.String()
var j []byte
diff --git a/outputs/tap.go b/outputs/tap.go
index 62dfb02a8..341e2a0ef 100644
--- a/outputs/tap.go
+++ b/outputs/tap.go
@@ -4,7 +4,6 @@ import (
"fmt"
"io"
"strconv"
- "time"
"github.com/goss-org/goss/resource"
"github.com/goss-org/goss/util"
@@ -13,11 +12,17 @@ import (
type Tap struct{}
func (r Tap) ValidOptions() []*formatOption {
- return []*formatOption{}
+ return []*formatOption{
+ {name: foSort},
+ }
}
func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult,
- startTime time.Time, outConfig util.OutputConfig) (exitCode int) {
+ outConfig util.OutputConfig) (exitCode int) {
+ includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)
+
+ sort := util.IsValueInList(foSort, outConfig.FormatOptions)
+ results = getResults(results, sort)
testCount := 0
failed := 0
@@ -29,12 +34,12 @@ func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult,
for _, testResult := range resultGroup {
switch testResult.Result {
case resource.SUCCESS:
- summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n"
+ summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
case resource.FAIL:
- summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n"
+ summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
failed++
case resource.SKIP:
- summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult2(testResult) + "\n"
+ summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult(testResult, true, includeRaw) + "\n"
default:
panic(fmt.Sprintf("Unexpected Result Code: %v\n", testResult.Result))
}
diff --git a/outputs/traces.go b/outputs/traces.go
index 1f5ceed1b..2a37d5da4 100644
--- a/outputs/traces.go
+++ b/outputs/traces.go
@@ -14,8 +14,8 @@ func logTrace(level string, msg string, testResult resource.TestResult, withIntR
testResult.ResourceType,
testResult.ResourceId,
testResult.Property,
- testResult.Expected,
- testResult.Found,
+ testResult.MatcherResult.Expected,
+ testResult.MatcherResult.Actual,
testResult.Duration.Seconds(),
testResult.Result,
)
@@ -26,8 +26,8 @@ func logTrace(level string, msg string, testResult resource.TestResult, withIntR
testResult.ResourceType,
testResult.ResourceId,
testResult.Property,
- testResult.Expected,
- testResult.Found,
+ testResult.MatcherResult.Expected,
+ testResult.MatcherResult.Actual,
testResult.Duration.Seconds(),
)
}
diff --git a/resource/addr.go b/resource/addr.go
index 49d2d7236..c4d4bd8bf 100644
--- a/resource/addr.go
+++ b/resource/addr.go
@@ -1,6 +1,7 @@
package resource
import (
+ "fmt"
"time"
"github.com/goss-org/goss/system"
@@ -10,7 +11,8 @@ import (
type Addr struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Address string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Address string `json:"address,omitempty" yaml:"address,omitempty"`
LocalAddress string `json:"local-address,omitempty" yaml:"local-address,omitempty"`
Reachable matcher `json:"reachable" yaml:"reachable"`
Timeout int `json:"timeout" yaml:"timeout"`
@@ -26,8 +28,13 @@ func init() {
registerResource(AddrResourceKey, &Addr{})
}
-func (a *Addr) ID() string { return a.Address }
-func (a *Addr) SetID(id string) { a.Address = id }
+func (a *Addr) ID() string {
+ if a.Address != "" && a.Address != a.id {
+ return fmt.Sprintf("%s: %s", a.id, a.Address)
+ }
+ return a.id
+}
+func (a *Addr) SetID(id string) { a.id = id }
func (a *Addr) SetSkip() { a.Skip = true }
func (a *Addr) TypeKey() string { return AddrResourceKey }
func (a *Addr) TypeName() string { return AddResourceName }
@@ -35,6 +42,12 @@ func (a *Addr) TypeName() string { return AddResourceName }
// FIXME: Can this be refactored?
func (a *Addr) GetTitle() string { return a.Title }
func (a *Addr) GetMeta() meta { return a.Meta }
+func (a *Addr) GetAddress() string {
+ if a.Address != "" {
+ return a.Address
+ }
+ return a.id
+}
func (a *Addr) Validate(sys *system.System) []TestResult {
skip := a.Skip
@@ -43,7 +56,7 @@ func (a *Addr) Validate(sys *system.System) []TestResult {
a.Timeout = 500
}
- sysAddr := sys.NewAddr(a.Address, sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress})
+ sysAddr := sys.NewAddr(a.GetAddress(), sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress})
var results []TestResult
results = append(results, ValidateValue(a, "reachable", a.Reachable, sysAddr.Reachable, skip))
@@ -54,7 +67,7 @@ func NewAddr(sysAddr system.Addr, config util.Config) (*Addr, error) {
address := sysAddr.Address()
reachable, err := sysAddr.Reachable()
a := &Addr{
- Address: address,
+ id: address,
Reachable: reachable,
Timeout: config.TimeOutMilliSeconds(),
LocalAddress: config.LocalAddress,
diff --git a/resource/command.go b/resource/command.go
index e69cb1a9e..424ded631 100644
--- a/resource/command.go
+++ b/resource/command.go
@@ -12,15 +12,15 @@ import (
)
type Command struct {
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
- Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Command string `json:"-" yaml:"-"`
- Exec string `json:"exec,omitempty" yaml:"exec,omitempty"`
- ExitStatus matcher `json:"exit-status" yaml:"exit-status"`
- Stdout []string `json:"stdout" yaml:"stdout"`
- Stderr []string `json:"stderr" yaml:"stderr"`
- Timeout int `json:"timeout" yaml:"timeout"`
- Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
+ Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Exec string `json:"exec,omitempty" yaml:"exec,omitempty"`
+ ExitStatus matcher `json:"exit-status" yaml:"exit-status"`
+ Stdout matcher `json:"stdout" yaml:"stdout"`
+ Stderr matcher `json:"stderr" yaml:"stderr"`
+ Timeout int `json:"timeout" yaml:"timeout"`
+ Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
}
const (
@@ -32,8 +32,8 @@ func init() {
registerResource(CommandResourceKey, &Command{})
}
-func (c *Command) ID() string { return c.Command }
-func (c *Command) SetID(id string) { c.Command = id }
+func (c *Command) ID() string { return c.id }
+func (c *Command) SetID(id string) { c.id = id }
func (c *Command) SetSkip() { c.Skip = true }
func (c *Command) TypeKey() string { return CommandResourceKey }
func (c *Command) TypeName() string { return CommandResourceName }
@@ -44,7 +44,7 @@ func (c *Command) GetExec() string {
if c.Exec != "" {
return c.Exec
}
- return c.Command
+ return c.id
}
func (c *Command) Validate(sys *system.System) []TestResult {
@@ -57,13 +57,13 @@ func (c *Command) Validate(sys *system.System) []TestResult {
var results []TestResult
sysCommand := sys.NewCommand(c.GetExec(), sys, util.Config{Timeout: time.Duration(c.Timeout) * time.Millisecond})
- cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.Command))
+ cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID()))
results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip))
- if len(c.Stdout) > 0 {
- results = append(results, ValidateContains(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
+ if isSet(c.Stdout) {
+ results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
}
- if len(c.Stderr) > 0 {
- results = append(results, ValidateContains(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
+ if isSet(c.Stderr) {
+ results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
}
return results
}
@@ -72,7 +72,7 @@ func NewCommand(sysCommand system.Command, config util.Config) (*Command, error)
command := sysCommand.Command()
exitStatus, err := sysCommand.ExitStatus()
c := &Command{
- Command: command,
+ id: command,
ExitStatus: exitStatus,
Stdout: []string{},
Stderr: []string{},
diff --git a/resource/dns.go b/resource/dns.go
index ab989b084..6ab100bef 100644
--- a/resource/dns.go
+++ b/resource/dns.go
@@ -1,6 +1,7 @@
package resource
import (
+ "fmt"
"strings"
"time"
@@ -11,7 +12,8 @@ import (
type DNS struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Host string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Resolve string `json:"resolve,omitempty" yaml:"resolve,omitempty"`
Resolveable matcher `json:"resolveable,omitempty" yaml:"resolveable,omitempty"`
Resolvable matcher `json:"resolvable" yaml:"resolvable"`
Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"`
@@ -29,13 +31,24 @@ func init() {
registerResource(DNSResourceKey, &DNS{})
}
-func (d *DNS) ID() string { return d.Host }
-func (d *DNS) SetID(id string) { d.Host = id }
+func (d *DNS) ID() string {
+ if d.Resolve != "" && d.Resolve != d.id {
+ return fmt.Sprintf("%s: %s", d.id, d.Resolve)
+ }
+ return d.id
+}
+func (d *DNS) SetID(id string) { d.id = id }
func (d *DNS) SetSkip() { d.Skip = true }
func (d *DNS) TypeKey() string { return DNSResourceKey }
func (d *DNS) TypeName() string { return DNSResourceName }
func (d *DNS) GetTitle() string { return d.Title }
func (d *DNS) GetMeta() meta { return d.Meta }
+func (d *DNS) GetResolve() string {
+ if d.Resolve != "" {
+ return d.Resolve
+ }
+ return d.id
+}
func (d *DNS) Validate(sys *system.System) []TestResult {
skip := d.Skip
@@ -43,7 +56,7 @@ func (d *DNS) Validate(sys *system.System) []TestResult {
d.Timeout = 500
}
- sysDNS := sys.NewDNS(d.Host, sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server})
+ sysDNS := sys.NewDNS(d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server})
var results []TestResult
// Backwards compatibility hack for now
@@ -72,7 +85,7 @@ func NewDNS(sysDNS system.DNS, config util.Config) (*DNS, error) {
server := sysDNS.Server()
d := &DNS{
- Host: host,
+ id: host,
Resolvable: resolvable,
Timeout: config.TimeOutMilliSeconds(),
Server: server,
diff --git a/resource/file.go b/resource/file.go
index 3b0e207e9..2d2fa66cb 100644
--- a/resource/file.go
+++ b/resource/file.go
@@ -1,26 +1,31 @@
package resource
import (
+ "fmt"
+ "os"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
type File struct {
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
- Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Path string `json:"-" yaml:"-"`
- Exists matcher `json:"exists" yaml:"exists"`
- Mode matcher `json:"mode,omitempty" yaml:"mode,omitempty"`
- Size matcher `json:"size,omitempty" yaml:"size,omitempty"`
- Owner matcher `json:"owner,omitempty" yaml:"owner,omitempty"`
- Group matcher `json:"group,omitempty" yaml:"group,omitempty"`
- LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"`
- Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"`
- Contains []string `json:"contains" yaml:"contains"`
- Md5 matcher `json:"md5,omitempty" yaml:"md5,omitempty"`
- Sha256 matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"`
- Sha512 matcher `json:"sha512,omitempty" yaml:"sha512,omitempty"`
- Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
+ Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Path string `json:"path,omitempty" yaml:"path,omitempty"`
+ Exists matcher `json:"exists" yaml:"exists"`
+ Mode matcher `json:"mode,omitempty" yaml:"mode,omitempty"`
+ Size matcher `json:"size,omitempty" yaml:"size,omitempty"`
+ Owner matcher `json:"owner,omitempty" yaml:"owner,omitempty"`
+ Group matcher `json:"group,omitempty" yaml:"group,omitempty"`
+ LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"`
+ Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"`
+ Contains matcher `json:"contains,omitempty" yaml:"contains,omitempty"`
+ Contents matcher `json:"contents" yaml:"contents"`
+ Md5 matcher `json:"md5,omitempty" yaml:"md5,omitempty"`
+ Sha256 matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"`
+ Sha512 matcher `json:"sha512,omitempty" yaml:"sha512,omitempty"`
+ Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
}
const (
@@ -32,18 +37,29 @@ func init() {
registerResource(FileResourceKey, &File{})
}
-func (f *File) ID() string { return f.Path }
-func (f *File) SetID(id string) { f.Path = id }
+func (f *File) ID() string {
+ if f.Path != "" && f.Path != f.id {
+ return fmt.Sprintf("%s: %s", f.id, f.Path)
+ }
+ return f.id
+}
+func (f *File) SetID(id string) { f.id = id }
func (f *File) SetSkip() { f.Skip = true }
func (f *File) TypeKey() string { return FileResourceKey }
func (f *File) TypeName() string { return FileResourceName }
func (f *File) GetTitle() string { return f.Title }
func (f *File) GetMeta() meta { return f.Meta }
+func (f *File) GetPath() string {
+ if f.Path != "" {
+ return f.Path
+ }
+ return f.id
+}
func (f *File) Validate(sys *system.System) []TestResult {
skip := f.Skip
- sysFile := sys.NewFile(f.Path, sys, util.Config{})
+ sysFile := sys.NewFile(f.GetPath(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(f, "exists", f.Exists, sysFile.Exists, skip))
@@ -65,8 +81,12 @@ func (f *File) Validate(sys *system.System) []TestResult {
if f.Filetype != nil {
results = append(results, ValidateValue(f, "filetype", f.Filetype, sysFile.Filetype, skip))
}
- if len(f.Contains) > 0 {
- results = append(results, ValidateContains(f, "contains", f.Contains, sysFile.Contains, skip))
+ if isSet(f.Contains) {
+ 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) {
+ results = append(results, ValidateValue(f, "contents", f.Contents, sysFile.Contents, skip))
}
if f.Size != nil {
results = append(results, ValidateValue(f, "size", f.Size, sysFile.Size, skip))
@@ -90,9 +110,9 @@ func NewFile(sysFile system.File, config util.Config) (*File, error) {
return nil, err
}
f := &File{
- Path: path,
+ id: path,
Exists: exists,
- Contains: []string{},
+ Contents: []string{},
}
if !contains(config.IgnoreList, "mode") {
if mode, err := sysFile.Mode(); err == nil {
@@ -119,10 +139,5 @@ func NewFile(sysFile system.File, config util.Config) (*File, error) {
f.Filetype = filetype
}
}
- if !contains(config.IgnoreList, "size") {
- if size, err := sysFile.Size(); err == nil {
- f.Size = size
- }
- }
return f, nil
}
diff --git a/resource/gomega.go b/resource/gomega.go
index 84bc75966..97da1026d 100644
--- a/resource/gomega.go
+++ b/resource/gomega.go
@@ -2,28 +2,29 @@ package resource
import (
"fmt"
- "sort"
"github.com/goss-org/goss/matchers"
-
- "github.com/onsi/gomega"
- "github.com/onsi/gomega/types"
)
-func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) {
+func matcherToGomegaMatcher(matcher any) (matchers.GossMatcher, error) {
+ // Default matchers
switch x := matcher.(type) {
- case string, int, bool, float64:
- return gomega.Equal(x), nil
+ case string:
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal(x)), nil
+ case float64, int:
+ return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", x)), nil
+ case bool:
+ return matchers.Equal(x), nil
case []any:
- var matchers []types.GomegaMatcher
- for _, valueI := range x {
- if subMatcher, ok := valueI.(types.GomegaMatcher); ok {
- matchers = append(matchers, subMatcher)
- } else {
- matchers = append(matchers, gomega.ContainElement(valueI))
- }
+ subMatchers, err := sliceToGomega(x)
+ if err != nil {
+ return nil, err
+ }
+ var interfaceSlice []any
+ for _, d := range subMatchers {
+ interfaceSlice = append(interfaceSlice, d)
}
- return gomega.And(matchers...), nil
+ return matchers.ContainElements(interfaceSlice...), nil
}
matcher = sanitizeExpectedValue(matcher)
if matcher == nil {
@@ -39,44 +40,77 @@ func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) {
break
}
switch matchType {
+ case "equal":
+ return matchers.Equal(value), nil
case "have-prefix":
- return gomega.HavePrefix(value.(string)), nil
+ v, isStr := value.(string)
+ if !isStr {
+ return nil, fmt.Errorf("have-prefix: syntax error: incorrect expectation type. expected string, got: %#v", value)
+ }
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix(v)), nil
case "have-suffix":
- return gomega.HaveSuffix(value.(string)), nil
+ v, isStr := value.(string)
+ if !isStr {
+ return nil, fmt.Errorf("have-suffix: syntax error: incorrect expectation type. expected string, got: %#v", value)
+ }
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix(v)), nil
case "match-regexp":
- return gomega.MatchRegexp(value.(string)), nil
+ v, isStr := value.(string)
+ if !isStr {
+ return nil, fmt.Errorf("match-regexp: syntax error: incorrect expectation type. expected string, got: %#v", value)
+ }
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp(v)), nil
+ case "contain-substring":
+ v, isStr := value.(string)
+ if !isStr {
+ return nil, fmt.Errorf("contain-substring: syntax error: incorrect expectation type. expected string, got: %#v", value)
+ }
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.ContainSubstring(v)), nil
case "have-len":
- value = sanitizeExpectedValue(value)
- return gomega.HaveLen(value.(int)), nil
- case "have-key-with-value":
- subMatchers, err := mapToGomega(value)
- if err != nil {
- return nil, err
+ v, isFloat := value.(float64)
+ if !isFloat {
+ return nil, fmt.Errorf("have-len: syntax error: incorrect expectation type. expected numeric, got: %#v", value)
}
- for key, val := range subMatchers {
- if val == nil {
- fmt.Printf("%d is nil", key)
- }
+ return matchers.HaveLen(int(v)), nil
+ case "have-patterns":
+ _, isArr := value.([]any)
+ if !isArr {
+ return nil, fmt.Errorf("have-patterns: syntax error: incorrect expectation type. expected array, got: %#v", value)
}
- return gomega.And(subMatchers...), nil
+ return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePatterns(value)), nil
case "have-key":
subMatcher, err := matcherToGomegaMatcher(value)
if err != nil {
return nil, err
}
- return gomega.HaveKey(subMatcher), nil
+ return matchers.HaveKey(subMatcher), nil
case "contain-element":
+ switch value.(type) {
+ case map[string]any, string, float64:
+ default:
+ return nil, fmt.Errorf("contain-element: syntax error: incorrect expectation type. expected matcher or value, got: %#v", value)
+ }
subMatcher, err := matcherToGomegaMatcher(value)
if err != nil {
return nil, err
}
- return gomega.ContainElement(subMatcher), nil
+ return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElement(subMatcher)), nil
+ case "contain-elements":
+ subMatchers, err := sliceToGomega(value)
+ if err != nil {
+ return nil, err
+ }
+ var interfaceSlice []any
+ for _, d := range subMatchers {
+ interfaceSlice = append(interfaceSlice, d)
+ }
+ return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElements(interfaceSlice...)), nil
case "not":
subMatcher, err := matcherToGomegaMatcher(value)
if err != nil {
return nil, err
}
- return gomega.Not(subMatcher), nil
+ return matchers.Not(subMatcher), nil
case "consist-of":
subMatchers, err := sliceToGomega(value)
if err != nil {
@@ -86,70 +120,55 @@ func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) {
for _, d := range subMatchers {
interfaceSlice = append(interfaceSlice, d)
}
- return gomega.ConsistOf(interfaceSlice...), nil
+ return matchers.ConsistOf(interfaceSlice...), nil
case "and":
subMatchers, err := sliceToGomega(value)
if err != nil {
return nil, err
}
- return gomega.And(subMatchers...), nil
+ return matchers.And(subMatchers...), nil
case "or":
subMatchers, err := sliceToGomega(value)
if err != nil {
return nil, err
}
- return gomega.Or(subMatchers...), nil
+ return matchers.Or(subMatchers...), nil
case "gt", "ge", "lt", "le":
- // Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead
- comparator := map[string]string{
- "gt": ">",
- "ge": ">=",
- "lt": "<",
- "le": "<=",
- }[matchType]
- return gomega.BeNumerically(comparator, value), nil
+ return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically(matchType, value)), nil
case "semver-constraint":
- return matchers.BeSemverConstraint(value.(string)), nil
- default:
- return nil, fmt.Errorf("Unknown matcher: %s", matchType)
-
- }
-}
-
-func mapToGomega(value any) (subMatchers []types.GomegaMatcher, err error) {
- valueI, ok := value.(map[string]any)
- if !ok {
- return nil, fmt.Errorf("Matcher expected map, got: %t", value)
- }
+ v, isStr := value.(string)
+ if !isStr {
+ return nil, fmt.Errorf("semver-contstraint: syntax error: incorrect expectation type. expected string, got: %#v", value)
+ }
+ return matchers.BeSemverConstraint(v), nil
+ case "gjson":
+ var subMatchers []matchers.GossMatcher
+ valueI, ok := value.(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("Matcher expected map, got: %t", value)
+ }
+ for key, val := range valueI {
+ subMatcher, err := matcherToGomegaMatcher(val)
+ if err != nil {
+ return nil, err
+ }
+ subMatchers = append(subMatchers, matchers.WithSafeTransform(matchers.Gjson{Path: key}, subMatcher))
- // Get keys
- keys := []string{}
- for key, _ := range valueI {
- keys = append(keys, key)
- }
- // Iterate through keys in a deterministic way, since ranging over a map
- // does not guarantee order
- sort.Strings(keys)
- for _, key := range keys {
- val := valueI[key]
- val, err = matcherToGomegaMatcher(val)
- if err != nil {
- return
}
+ return matchers.And(subMatchers...), nil
+ default:
+ return nil, fmt.Errorf("Unknown matcher: %s", matchType)
- subMatcher := gomega.HaveKeyWithValue(key, val)
- subMatchers = append(subMatchers, subMatcher)
}
- return
}
-func sliceToGomega(value any) ([]types.GomegaMatcher, error) {
+func sliceToGomega(value any) ([]matchers.GossMatcher, error) {
valueI, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("Matcher expected array, got: %t", value)
}
- var subMatchers []types.GomegaMatcher
+ var subMatchers []matchers.GossMatcher
for _, v := range valueI {
subMatcher, err := matcherToGomegaMatcher(v)
if err != nil {
@@ -160,16 +179,17 @@ func sliceToGomega(value any) ([]types.GomegaMatcher, error) {
return subMatchers, nil
}
-// Normalize expectedValue so json and yaml are the same
+// sanitizeExpectedValue normalizes the value so json and yaml are the same
func sanitizeExpectedValue(i any) any {
- if e, ok := i.(float64); ok {
- return int(e)
+ if e, ok := i.(int); ok {
+ return float64(e)
}
if e, ok := i.(map[any]any); ok {
out := make(map[string]any)
for k, v := range e {
ks, ok := k.(string)
if !ok {
+ // We should never get here
panic(fmt.Sprintf("Matcher key type not string: %T\n\n", k))
}
out[ks] = sanitizeExpectedValue(v)
diff --git a/resource/gomega_test.go b/resource/gomega_test.go
index c1ace3f7c..bccb3a41c 100644
--- a/resource/gomega_test.go
+++ b/resource/gomega_test.go
@@ -2,15 +2,10 @@ package resource
import (
"encoding/json"
- "fmt"
- "reflect"
- "regexp"
"testing"
"github.com/goss-org/goss/matchers"
-
- "github.com/onsi/gomega"
- "github.com/onsi/gomega/types"
+ "github.com/stretchr/testify/assert"
)
var gomegaTests = []struct {
@@ -21,20 +16,22 @@ var gomegaTests = []struct {
// Default for simple types
{
in: `"foo"`,
- want: gomega.Equal("foo"),
+ want: matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")),
},
{
in: `1`,
- want: gomega.Equal(float64(1)),
+ want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", float64(1))),
},
{
in: `true`,
- want: gomega.Equal(true),
+ want: matchers.Equal(true),
},
// Default for Array
{
- in: `["foo", "bar"]`,
- want: gomega.And(gomega.ContainElement("foo"), gomega.ContainElement("bar")),
+ in: `["foo", "bar"]`,
+ want: matchers.ContainElements(
+ matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")),
+ matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("bar"))),
useNegateTester: true,
},
@@ -42,91 +39,103 @@ var gomegaTests = []struct {
// Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead
{
in: `{"gt": 1}`,
- want: gomega.BeNumerically(">", float64(1)),
+ want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("gt", float64(1))),
},
{
in: `{"ge": 1}`,
- want: gomega.BeNumerically(">=", float64(1)),
+ want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("ge", float64(1))),
},
{
in: `{"lt": 1}`,
- want: gomega.BeNumerically("<", float64(1)),
+ want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("lt", float64(1))),
},
{
in: `{"le": 1}`,
- want: gomega.BeNumerically("<=", float64(1)),
+ want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("le", float64(1))),
},
// String
{
in: `{"have-prefix": "foo"}`,
- want: gomega.HavePrefix("foo"),
+ want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix("foo")),
},
{
in: `{"have-suffix": "foo"}`,
- want: gomega.HaveSuffix("foo"),
+ want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix("foo")),
},
// Regex support is based on golangs regex engine https://golang.org/pkg/regexp/syntax/
{
in: `{"match-regexp": "foo"}`,
- want: gomega.MatchRegexp("foo"),
+ want: matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp("foo")),
},
// Collection
{
in: `{"consist-of": ["foo"]}`,
- want: gomega.ConsistOf(gomega.Equal("foo")),
+ want: matchers.ConsistOf(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
},
{
- in: `{"contain-element": "foo"}`,
- want: gomega.ContainElement(gomega.Equal("foo")),
+ in: `{"contain-element": "foo"}`,
+ want: matchers.WithSafeTransform(matchers.ToArray{},
+ matchers.ContainElement(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo")))),
},
{
in: `{"have-len": 3}`,
- want: gomega.HaveLen(3),
- },
- {
- in: `{"have-key-with-value": { "foo": 1, "bar": "baz" }}`,
- // Keys are sorted and then passed to gomega.And so the order
- // of the conditions in this `want` is important
- want: gomega.And(
- gomega.HaveKeyWithValue("bar", gomega.Equal("baz")),
- gomega.HaveKeyWithValue("foo", gomega.Equal(1)),
- ),
- useNegateTester: true,
+ want: matchers.HaveLen(3),
},
{
in: `{"have-key": "foo"}`,
- want: gomega.HaveKey(gomega.Equal("foo")),
+ want: matchers.HaveKey(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
},
// Negation
{
in: `{"not": "foo"}`,
- want: gomega.Not(gomega.Equal("foo")),
+ want: matchers.Not(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
},
// Complex logic
{
- in: `{"and": ["foo", "foo"]}`,
- want: gomega.And(gomega.Equal("foo"), gomega.Equal("foo")),
+ in: `{"and": ["foo", "foo"]}`,
+ want: matchers.And(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo")),
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo")),
+ ),
useNegateTester: true,
},
{
- in: `{"and": [{"have-prefix": "foo"}, "foo"]}`,
- want: gomega.And(gomega.HavePrefix("foo"), gomega.Equal("foo")),
+ in: `{"and": [{"have-prefix": "foo"}, "foo"]}`,
+ want: matchers.And(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.HavePrefix("foo")),
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo")),
+ ),
useNegateTester: true,
},
{
- in: `{"not": {"have-prefix": "foo"}}`,
- want: gomega.Not(gomega.HavePrefix("foo")),
+ in: `{"not": {"have-prefix": "foo"}}`,
+ want: matchers.Not(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.HavePrefix("foo"))),
},
{
- in: `{"or": ["foo", "foo"]}`,
- want: gomega.Or(gomega.Equal("foo"), gomega.Equal("foo")),
+ in: `{"or": ["foo", "foo"]}`,
+ want: matchers.Or(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo")),
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.Equal("foo"))),
},
{
- in: `{"not": {"and": [{"have-prefix": "foo"}]}}`,
- want: gomega.Not(gomega.And(gomega.HavePrefix("foo"))),
+ in: `{"not": {"and": [{"have-prefix": "foo"}]}}`,
+ want: matchers.Not(
+ matchers.And(
+ matchers.WithSafeTransform(matchers.ToString{},
+ matchers.HavePrefix("foo")))),
},
// Semver Constraint
@@ -151,34 +160,5 @@ func TestMatcherToGomegaMatcher(t *testing.T) {
}
func gomegaTestEqual(t *testing.T, got, want any, useNegateTester bool, in string) {
- if !gomegaEqual(got, want, useNegateTester) {
- t.Errorf("For input '%s': got %T %v, want %T %v", in, got, got, want, want)
- }
-}
-func gomegaEqual(g, w any, negateTester bool) bool {
- gotT := reflect.TypeOf(g)
- wantT := reflect.TypeOf(w)
- got := g.(types.GomegaMatcher)
- want := w.(types.GomegaMatcher)
- var gotMessage string
- var wantMessage string
- if negateTester {
- gotMessage = got.NegatedFailureMessage("foo")
- wantMessage = want.NegatedFailureMessage("foo")
- } else {
- gotMessage = got.FailureMessage("foo")
- wantMessage = want.FailureMessage("foo")
- }
- gotMessage = sanitizeMatcherText(gotMessage)
- wantMessage = sanitizeMatcherText(wantMessage)
- fmt.Println("got:", gotMessage)
- fmt.Println("want:", wantMessage)
-
- return gotT == wantT &&
- gotMessage == wantMessage
-}
-
-func sanitizeMatcherText(s string) string {
- r := regexp.MustCompile("[0-9]x[a-z0-9]{10}")
- return r.ReplaceAllString(s, "")
+ assert.Equal(t, got, want)
}
diff --git a/resource/group.go b/resource/group.go
index 58c8f6a64..7142fb69c 100644
--- a/resource/group.go
+++ b/resource/group.go
@@ -10,7 +10,8 @@ import (
type Group struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Groupname string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Groupname string `json:"groupname,omitempty" yaml:"groupname,omitempty"`
Exists matcher `json:"exists" yaml:"exists"`
GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
@@ -25,17 +26,28 @@ func init() {
registerResource(GroupResourceKey, &Group{})
}
-func (g *Group) ID() string { return g.Groupname }
-func (g *Group) SetID(id string) { g.Groupname = id }
+func (g *Group) ID() string {
+ if g.Groupname != "" && g.Groupname != g.id {
+ return fmt.Sprintf("%s: %s", g.id, g.Groupname)
+ }
+ return g.id
+}
+func (g *Group) SetID(id string) { g.id = id }
func (g *Group) SetSkip() { g.Skip = true }
func (g *Group) TypeKey() string { return GroupResourceKey }
func (g *Group) TypeName() string { return GroupResourceName }
func (g *Group) GetTitle() string { return g.Title }
func (g *Group) GetMeta() meta { return g.Meta }
+func (g *Group) GetGroupname() string {
+ if g.Groupname != "" {
+ return g.Groupname
+ }
+ return g.id
+}
func (g *Group) Validate(sys *system.System) []TestResult {
skip := g.Skip
- sysgroup := sys.NewGroup(g.Groupname, sys, util.Config{})
+ sysgroup := sys.NewGroup(g.GetGroupname(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(g, "exists", g.Exists, sysgroup.Exists, skip))
@@ -43,7 +55,7 @@ func (g *Group) Validate(sys *system.System) []TestResult {
skip = true
}
if g.GID != nil {
- gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.Groupname))
+ gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.ID()))
results = append(results, ValidateValue(g, "gid", gGID, sysgroup.GID, skip))
}
return results
@@ -53,8 +65,8 @@ func NewGroup(sysGroup system.Group, config util.Config) (*Group, error) {
groupname := sysGroup.Groupname()
exists, _ := sysGroup.Exists()
g := &Group{
- Groupname: groupname,
- Exists: exists,
+ id: groupname,
+ Exists: exists,
}
if !contains(config.IgnoreList, "stderr") {
if gid, err := sysGroup.GID(); err == nil {
diff --git a/resource/http.go b/resource/http.go
index b43ff06b8..441636e5a 100644
--- a/resource/http.go
+++ b/resource/http.go
@@ -1,6 +1,7 @@
package resource
import (
+ "fmt"
"time"
"github.com/goss-org/goss/system"
@@ -9,18 +10,18 @@ import (
type HTTP struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
- URL string `json:"url,omitempty" yaml:"url,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- HTTP string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ URL string `json:"url,omitempty" yaml:"url,omitempty"`
Method string `json:"method,omitempty" yaml:"method,omitempty"`
Status matcher `json:"status" yaml:"status"`
AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"`
NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"`
- Timeout int `json:"timeout" yaml:"timeout"`
+ Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"`
RequestHeader []string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"`
- RequestBody string `json:"request-body,omitemptyy" yaml:"request-body,omitempty"`
- Headers []string `json:"headers,omitempty" yaml:"headers,omitempty"`
- Body []string `json:"body" yaml:"body"`
+ RequestBody string `json:"request-body,omitempty" yaml:"request-body,omitempty"`
+ Headers matcher `json:"headers,omitempty" yaml:"headers,omitempty"`
+ Body matcher `json:"body,omitempty" yaml:"body,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
CAFile string `json:"ca-file,omitempty" yaml:"ca-file,omitempty"`
@@ -39,22 +40,25 @@ func init() {
registerResource(HTTPResourceKey, &HTTP{})
}
-func (u *HTTP) ID() string { return u.HTTP }
-
-func (u *HTTP) SetID(id string) { u.HTTP = id }
+func (h *HTTP) ID() string {
+ if h.URL != "" && h.URL != h.id {
+ return fmt.Sprintf("%s: %s", h.id, h.URL)
+ }
+ return h.id
+}
+func (h *HTTP) SetID(id string) { h.id = id }
func (u *HTTP) SetSkip() { u.Skip = true }
func (u *HTTP) TypeKey() string { return HTTPResourceKey }
func (u *HTTP) TypeName() string { return HTTPResourceName }
// FIXME: Can this be refactored?
-func (u *HTTP) GetTitle() string { return u.Title }
-func (u *HTTP) GetMeta() meta { return u.Meta }
-
-func (u *HTTP) getURL() string {
- if u.URL != "" {
- return u.URL
+func (r *HTTP) GetTitle() string { return r.Title }
+func (r *HTTP) GetMeta() meta { return r.Meta }
+func (r *HTTP) getURL() string {
+ if r.URL != "" {
+ return r.URL
}
- return u.HTTP
+ return r.id
}
func (u *HTTP) Validate(sys *system.System) []TestResult {
@@ -78,11 +82,11 @@ func (u *HTTP) Validate(sys *system.System) []TestResult {
if shouldSkip(results) {
skip = true
}
- if len(u.Headers) > 0 {
- results = append(results, ValidateContains(u, "Headers", u.Headers, sysHTTP.Headers, skip))
+ if isSet(u.Headers) {
+ results = append(results, ValidateValue(u, "Headers", u.Headers, sysHTTP.Headers, skip))
}
- if len(u.Body) > 0 {
- results = append(results, ValidateContains(u, "Body", u.Body, sysHTTP.Body, skip))
+ if isSet(u.Body) {
+ results = append(results, ValidateValue(u, "Body", u.Body, sysHTTP.Body, skip))
}
return results
@@ -92,10 +96,10 @@ func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) {
http := sysHTTP.HTTP()
status, err := sysHTTP.Status()
u := &HTTP{
- HTTP: http,
+ id: http,
Status: status,
RequestHeader: []string{},
- Headers: []string{},
+ Headers: nil,
Body: []string{},
AllowInsecure: config.AllowInsecure,
NoFollowRedirects: config.NoFollowRedirects,
diff --git a/resource/interface.go b/resource/interface.go
index 94012408f..c26cb66b2 100644
--- a/resource/interface.go
+++ b/resource/interface.go
@@ -1,6 +1,8 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
@@ -8,7 +10,8 @@ import (
type Interface struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Name string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
Exists matcher `json:"exists" yaml:"exists"`
Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"`
MTU matcher `json:"mtu,omitempty" yaml:"mtu,omitempty"`
@@ -24,8 +27,13 @@ func init() {
registerResource(InterfaceResourceKey, &Interface{})
}
-func (i *Interface) ID() string { return i.Name }
-func (i *Interface) SetID(id string) { i.Name = id }
+func (i *Interface) ID() string {
+ if i.Name != "" && i.Name != i.id {
+ return fmt.Sprintf("%s: %s", i.id, i.Name)
+ }
+ return i.id
+}
+func (i *Interface) SetID(id string) { i.id = id }
func (i *Interface) SetSkip() { i.Skip = true }
func (i *Interface) TypeKey() string { return InterfaceResourceKey }
func (i *Interface) TypeName() string { return InterfaceResourceName }
@@ -33,10 +41,16 @@ func (i *Interface) TypeName() string { return InterfaceResourceName }
// FIXME: Can this be refactored?
func (i *Interface) GetTitle() string { return i.Title }
func (i *Interface) GetMeta() meta { return i.Meta }
+func (i *Interface) GetName() string {
+ if i.Name != "" {
+ return i.Name
+ }
+ return i.id
+}
func (i *Interface) Validate(sys *system.System) []TestResult {
skip := i.Skip
- sysInterface := sys.NewInterface(i.Name, sys, util.Config{})
+ sysInterface := sys.NewInterface(i.GetName(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(i, "exists", i.Exists, sysInterface.Exists, skip))
@@ -56,7 +70,7 @@ func NewInterface(sysInterface system.Interface, config util.Config) (*Interface
name := sysInterface.Name()
exists, _ := sysInterface.Exists()
i := &Interface{
- Name: name,
+ id: name,
Exists: exists,
}
if !contains(config.IgnoreList, "addrs") {
diff --git a/resource/kernel_param.go b/resource/kernel_param.go
index 4f886d96e..a8e3d9f56 100644
--- a/resource/kernel_param.go
+++ b/resource/kernel_param.go
@@ -1,6 +1,8 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
@@ -8,6 +10,8 @@ import (
type KernelParam struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
Key string `json:"-" yaml:"-"`
Value matcher `json:"value" yaml:"value"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
@@ -22,22 +26,34 @@ func init() {
registerResource(KernelParamResourceKey, &KernelParam{})
}
-func (a *KernelParam) ID() string { return a.Key }
-func (a *KernelParam) SetID(id string) { a.Key = id }
+func (k *KernelParam) ID() string {
+ if k.Name != "" && k.Name != k.id {
+ return fmt.Sprintf("%s: %s", k.id, k.Name)
+ }
+ return k.id
+}
+func (a *KernelParam) SetID(id string) { a.id = id }
+
func (a *KernelParam) SetSkip() { a.Skip = true }
func (a *KernelParam) TypeKey() string { return KernelParamResourceKey }
func (a *KernelParam) TypeName() string { return KernelParamResourceName }
// FIXME: Can this be refactored?
-func (a *KernelParam) GetTitle() string { return a.Title }
-func (a *KernelParam) GetMeta() meta { return a.Meta }
+func (k *KernelParam) GetTitle() string { return k.Title }
+func (k *KernelParam) GetMeta() meta { return k.Meta }
+func (k *KernelParam) GetName() string {
+ if k.Name != "" {
+ return k.Name
+ }
+ return k.id
+}
-func (a *KernelParam) Validate(sys *system.System) []TestResult {
- skip := a.Skip
- sysKernelParam := sys.NewKernelParam(a.Key, sys, util.Config{})
+func (k *KernelParam) Validate(sys *system.System) []TestResult {
+ skip := k.Skip
+ sysKernelParam := sys.NewKernelParam(k.GetName(), sys, util.Config{})
var results []TestResult
- results = append(results, ValidateValue(a, "value", a.Value, sysKernelParam.Value, skip))
+ results = append(results, ValidateValue(k, "value", k.Value, sysKernelParam.Value, skip))
return results
}
@@ -45,7 +61,7 @@ func NewKernelParam(sysKernelParam system.KernelParam, config util.Config) (*Ker
key := sysKernelParam.Key()
value, err := sysKernelParam.Value()
a := &KernelParam{
- Key: key,
+ id: key,
Value: value,
}
return a, err
diff --git a/resource/matching.go b/resource/matching.go
index b70bdcdf7..5b608753c 100644
--- a/resource/matching.go
+++ b/resource/matching.go
@@ -3,6 +3,7 @@ package resource
import (
"encoding/json"
"fmt"
+ "io"
"reflect"
"strings"
@@ -11,11 +12,13 @@ import (
)
type Matching struct {
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
- Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Content any `json:"content,omitempty" yaml:"content,omitempty"`
- Id string `json:"-" yaml:"-"`
- Matches matcher `json:"matches" yaml:"matches"`
+ Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ Content any `json:"content,omitempty" yaml:"content,omitempty"`
+ AsReader bool `json:"as-reader,omitempty" yaml:"as-reader,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Matches matcher `json:"matches" yaml:"matches"`
+ Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
}
const (
@@ -25,8 +28,8 @@ const (
type MatchingMap map[string]*Matching
-func (a *Matching) ID() string { return a.Id }
-func (a *Matching) SetID(id string) { a.Id = id }
+func (a *Matching) ID() string { return a.id }
+func (a *Matching) SetID(id string) { a.id = id }
func (a *Matching) SetSkip() {}
func (a *Matching) TypeKey() string { return MatchingResourceKey }
func (a *Matching) TypeName() string { return MatchingResourceName }
@@ -37,10 +40,22 @@ func (r *Matching) GetMeta() meta { return r.Meta }
func (a *Matching) Validate(sys *system.System) []TestResult {
skip := false
+ if a.Skip {
+ skip = true
+ }
- // ValidateValue expects a function
- stub := func() (any, error) {
- return a.Content, nil
+ var stub interface{}
+ if a.AsReader {
+ s := fmt.Sprintf("%v", a.Content)
+ // ValidateValue expects a function
+ stub = func() (io.Reader, error) {
+ return strings.NewReader(s), nil
+ }
+ } else {
+ // ValidateValue expects a function
+ stub = func() (any, error) {
+ return a.Content, nil
+ }
}
var results []TestResult
diff --git a/resource/mount.go b/resource/mount.go
index 09709362f..cf8850f34 100644
--- a/resource/mount.go
+++ b/resource/mount.go
@@ -1,6 +1,8 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
@@ -8,7 +10,8 @@ import (
type Mount struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- MountPoint string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ MountPoint string `json:"mountpoint,omitempty" yaml:"mountpoint,omitempty"`
Exists matcher `json:"exists" yaml:"exists"`
Opts matcher `json:"opts,omitempty" yaml:"opts,omitempty"`
Source matcher `json:"source,omitempty" yaml:"source,omitempty"`
@@ -26,8 +29,13 @@ func init() {
registerResource(MountResourceKey, &Mount{})
}
-func (m *Mount) ID() string { return m.MountPoint }
-func (m *Mount) SetID(id string) { m.MountPoint = id }
+func (m *Mount) ID() string {
+ if m.MountPoint != "" && m.MountPoint != m.id {
+ return fmt.Sprintf("%s: %s", m.id, m.MountPoint)
+ }
+ return m.id
+}
+func (m *Mount) SetID(id string) { m.id = id }
func (m *Mount) SetSkip() { m.Skip = true }
func (m *Mount) TypeKey() string { return MountResourceKey }
func (m *Mount) TypeName() string { return MountResourceName }
@@ -35,10 +43,16 @@ func (m *Mount) TypeName() string { return MountResourceName }
// FIXME: Can this be refactored?
func (m *Mount) GetTitle() string { return m.Title }
func (m *Mount) GetMeta() meta { return m.Meta }
+func (m *Mount) GetMountPoint() string {
+ if m.MountPoint != "" {
+ return m.MountPoint
+ }
+ return m.id
+}
func (m *Mount) Validate(sys *system.System) []TestResult {
skip := m.Skip
- sysMount := sys.NewMount(m.MountPoint, sys, util.Config{})
+ sysMount := sys.NewMount(m.GetMountPoint(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(m, "exists", m.Exists, sysMount.Exists, skip))
@@ -64,8 +78,8 @@ func NewMount(sysMount system.Mount, config util.Config) (*Mount, error) {
mountPoint := sysMount.MountPoint()
exists, _ := sysMount.Exists()
m := &Mount{
- MountPoint: mountPoint,
- Exists: exists,
+ id: mountPoint,
+ Exists: exists,
}
if !contains(config.IgnoreList, "opts") {
if opts, err := sysMount.Opts(); err == nil {
diff --git a/resource/package.go b/resource/package.go
index 8678c8e85..aed11165c 100644
--- a/resource/package.go
+++ b/resource/package.go
@@ -1,6 +1,8 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
@@ -8,7 +10,8 @@ import (
type Package struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Name string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
Installed matcher `json:"installed" yaml:"installed"`
Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
@@ -23,17 +26,28 @@ func init() {
registerResource(PackageResourceKey, &Package{})
}
-func (p *Package) ID() string { return p.Name }
-func (p *Package) SetID(id string) { p.Name = id }
+func (p *Package) ID() string {
+ if p.Name != "" && p.Name != p.id {
+ return fmt.Sprintf("%s: %s", p.id, p.Name)
+ }
+ return p.id
+}
+func (p *Package) SetID(id string) { p.id = id }
func (p *Package) SetSkip() { p.Skip = true }
func (p *Package) TypeKey() string { return PackageResourceKey }
func (p *Package) TypeName() string { return PackageResourceName }
func (p *Package) GetTitle() string { return p.Title }
func (p *Package) GetMeta() meta { return p.Meta }
+func (p *Package) GetName() string {
+ if p.Name != "" {
+ return p.Name
+ }
+ return p.id
+}
func (p *Package) Validate(sys *system.System) []TestResult {
skip := p.Skip
- sysPkg := sys.NewPackage(p.Name, sys, util.Config{})
+ sysPkg := sys.NewPackage(p.GetName(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip))
@@ -50,7 +64,7 @@ func NewPackage(sysPackage system.Package, config util.Config) (*Package, error)
name := sysPackage.Name()
installed, _ := sysPackage.Installed()
p := &Package{
- Name: name,
+ id: name,
Installed: installed,
}
if !contains(config.IgnoreList, "versions") {
diff --git a/resource/port.go b/resource/port.go
index 7fd245cc8..ce64666a4 100644
--- a/resource/port.go
+++ b/resource/port.go
@@ -1,6 +1,8 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
@@ -8,7 +10,8 @@ import (
type Port struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Port string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Port string `json:"port,omitempty" yaml:"port,omitempty"`
Listening matcher `json:"listening" yaml:"listening"`
IP matcher `json:"ip,omitempty" yaml:"ip,omitempty"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
@@ -23,17 +26,28 @@ func init() {
registerResource(PortResourceKey, &Port{})
}
-func (p *Port) ID() string { return p.Port }
-func (p *Port) SetID(id string) { p.Port = id }
+func (p *Port) ID() string {
+ if p.Port != "" && p.Port != p.id {
+ return fmt.Sprintf("%s: %s", p.id, p.Port)
+ }
+ return p.id
+}
+func (p *Port) SetID(id string) { p.id = id }
func (p *Port) SetSkip() { p.Skip = true }
func (p *Port) TypeKey() string { return PortResourceKey }
func (p *Port) TypeName() string { return PortResourceName }
func (p *Port) GetTitle() string { return p.Title }
func (p *Port) GetMeta() meta { return p.Meta }
+func (p *Port) GetPort() string {
+ if p.Port != "" {
+ return p.Port
+ }
+ return p.id
+}
func (p *Port) Validate(sys *system.System) []TestResult {
skip := p.Skip
- sysPort := sys.NewPort(p.Port, sys, util.Config{})
+ sysPort := sys.NewPort(p.GetPort(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(p, "listening", p.Listening, sysPort.Listening, skip))
@@ -50,7 +64,7 @@ func NewPort(sysPort system.Port, config util.Config) (*Port, error) {
port := sysPort.Port()
listening, _ := sysPort.Listening()
p := &Port{
- Port: port,
+ id: port,
Listening: listening,
}
if !contains(config.IgnoreList, "ip") {
diff --git a/resource/process.go b/resource/process.go
index 27cafded0..0dc369c99 100644
--- a/resource/process.go
+++ b/resource/process.go
@@ -1,16 +1,19 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
type Process struct {
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
- Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Executable string `json:"-" yaml:"-"`
- Running matcher `json:"running" yaml:"running"`
- Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
+ Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Comm string `json:"comm,omitempty" yaml:"comm,omitempty"`
+ Running matcher `json:"running" yaml:"running"`
+ Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
}
const (
@@ -22,17 +25,28 @@ func init() {
registerResource(ProcessResourceKey, &Process{})
}
-func (p *Process) ID() string { return p.Executable }
-func (p *Process) SetID(id string) { p.Executable = id }
+func (p *Process) ID() string {
+ if p.Comm != "" && p.Comm != p.id {
+ return fmt.Sprintf("%s: %s", p.id, p.Comm)
+ }
+ return p.id
+}
+func (p *Process) SetID(id string) { p.id = id }
func (p *Process) SetSkip() { p.Skip = true }
func (p *Process) TypeKey() string { return ProcessResourceKey }
func (p *Process) TypeName() string { return ProcessResourceName }
func (p *Process) GetTitle() string { return p.Title }
func (p *Process) GetMeta() meta { return p.Meta }
+func (p *Process) GetComm() string {
+ if p.Comm != "" {
+ return p.Comm
+ }
+ return p.id
+}
func (p *Process) Validate(sys *system.System) []TestResult {
skip := p.Skip
- sysProcess := sys.NewProcess(p.Executable, sys, util.Config{})
+ sysProcess := sys.NewProcess(p.GetComm(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(p, "running", p.Running, sysProcess.Running, skip))
@@ -46,7 +60,7 @@ func NewProcess(sysProcess system.Process, config util.Config) (*Process, error)
return nil, err
}
return &Process{
- Executable: executable,
- Running: running,
+ id: executable,
+ Running: running,
}, nil
}
diff --git a/resource/resource.go b/resource/resource.go
index 57477b2eb..bff257e02 100644
--- a/resource/resource.go
+++ b/resource/resource.go
@@ -2,6 +2,7 @@ package resource
import (
"fmt"
+ "os"
"path/filepath"
"strconv"
"strings"
@@ -57,7 +58,7 @@ func deprecateAtoI(depr any, desc string) any {
if !ok {
return depr
}
- fmt.Printf("DEPRECATION WARNING: %s should be an integer not a string\n", desc)
+ fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: %s should be an integer not a string\n", desc)
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
@@ -81,14 +82,17 @@ func shouldSkip(results []TestResult) bool {
if len(results) < 1 {
return false
}
- if results[0].Err != nil {
- return true
- }
- if len(results[0].Found) < 1 {
- return false
- }
- if results[0].Found[0] == "false" {
+ if results[0].Err != nil || results[0].Result != SUCCESS || results[0].MatcherResult.Actual == false {
return true
}
return false
}
+
+func isSet(i interface{}) bool {
+ switch v := i.(type) {
+ case []interface{}:
+ return len(v) > 0
+ default:
+ return i != nil
+ }
+}
diff --git a/resource/service.go b/resource/service.go
index 2163e6d6f..84b334050 100644
--- a/resource/service.go
+++ b/resource/service.go
@@ -1,17 +1,21 @@
package resource
import (
+ "fmt"
+
"github.com/goss-org/goss/system"
"github.com/goss-org/goss/util"
)
type Service struct {
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
- Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Service string `json:"-" yaml:"-"`
- Enabled matcher `json:"enabled" yaml:"enabled"`
- Running matcher `json:"running" yaml:"running"`
- Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
+ Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
+ id string `json:"-" yaml:"-"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
+ Enabled matcher `json:"enabled" yaml:"enabled"`
+ Running matcher `json:"running" yaml:"running"`
+ Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
+ RunLevels matcher `json:"runlevels,omitempty" yaml:"runlevels,omitempty"`
}
const (
@@ -23,21 +27,39 @@ func init() {
registerResource(ServiceResourceKey, &Service{})
}
-func (s *Service) ID() string { return s.Service }
-func (s *Service) SetID(id string) { s.Service = id }
+func (s *Service) ID() string {
+ if s.Name != "" && s.Name != s.id {
+ return fmt.Sprintf("%s: %s", s.id, s.Name)
+ }
+ return s.id
+}
+func (s *Service) SetID(id string) { s.id = id }
func (s *Service) SetSkip() { s.Skip = true }
func (s *Service) TypeKey() string { return ServiceResourceKey }
func (s *Service) TypeName() string { return ServiceResourceName }
func (s *Service) GetTitle() string { return s.Title }
func (s *Service) GetMeta() meta { return s.Meta }
+func (s *Service) GetName() string {
+ if s.Name != "" {
+ return s.Name
+ }
+ return s.id
+}
func (s *Service) Validate(sys *system.System) []TestResult {
skip := s.Skip
- sysservice := sys.NewService(s.Service, sys, util.Config{})
+ sysservice := sys.NewService(s.GetName(), sys, util.Config{})
var results []TestResult
- results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip))
- results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip))
+ if s.Enabled != nil {
+ results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip))
+ }
+ if s.Running != nil {
+ results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip))
+ }
+ if s.RunLevels != nil {
+ results = append(results, ValidateValue(s, "runlevels", s.RunLevels, sysservice.RunLevels, skip))
+ }
return results
}
@@ -52,7 +74,7 @@ func NewService(sysService system.Service, config util.Config) (*Service, error)
return nil, err
}
return &Service{
- Service: service,
+ id: service,
Enabled: enabled,
Running: running,
}, nil
diff --git a/resource/user.go b/resource/user.go
index c11c749a0..d8ac634e2 100644
--- a/resource/user.go
+++ b/resource/user.go
@@ -10,7 +10,8 @@ import (
type User struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
- Username string `json:"-" yaml:"-"`
+ id string `json:"-" yaml:"-"`
+ Username string `json:"username,omitempty" yaml:"username,omitempty"`
Exists matcher `json:"exists" yaml:"exists"`
UID matcher `json:"uid,omitempty" yaml:"uid,omitempty"`
GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"`
@@ -29,17 +30,28 @@ func init() {
registerResource(UserResourceKey, &User{})
}
-func (u *User) ID() string { return u.Username }
-func (u *User) SetID(id string) { u.Username = id }
+func (u *User) ID() string {
+ if u.Username != "" && u.Username != u.id {
+ return fmt.Sprintf("%s: %s", u.id, u.Username)
+ }
+ return u.id
+}
+func (u *User) SetID(id string) { u.id = id }
func (u *User) SetSkip() { u.Skip = true }
func (u *User) TypeKey() string { return UserResourceKey }
func (u *User) TypeName() string { return UserResourceName }
func (u *User) GetTitle() string { return u.Title }
func (u *User) GetMeta() meta { return u.Meta }
+func (u *User) GetUsername() string {
+ if u.Username != "" {
+ return u.Username
+ }
+ return u.id
+}
func (u *User) Validate(sys *system.System) []TestResult {
skip := u.Skip
- sysuser := sys.NewUser(u.Username, sys, util.Config{})
+ sysuser := sys.NewUser(u.GetUsername(), sys, util.Config{})
var results []TestResult
results = append(results, ValidateValue(u, "exists", u.Exists, sysuser.Exists, skip))
@@ -70,8 +82,8 @@ func NewUser(sysUser system.User, config util.Config) (*User, error) {
username := sysUser.Username()
exists, _ := sysUser.Exists()
u := &User{
- Username: username,
- Exists: exists,
+ id: username,
+ Exists: exists,
}
if !contains(config.IgnoreList, "uid") {
if uid, err := sysUser.UID(); err == nil {
diff --git a/resource/validate.go b/resource/validate.go
index 55cf8c1e6..95525eaf3 100644
--- a/resource/validate.go
+++ b/resource/validate.go
@@ -1,16 +1,13 @@
package resource
import (
- "bufio"
- "encoding/json"
"fmt"
"io"
"reflect"
- "regexp"
"strings"
"time"
- "github.com/onsi/gomega/types"
+ "github.com/goss-org/goss/matchers"
)
const (
@@ -60,20 +57,24 @@ func toValidateError(err error) *ValidateError {
}
type TestResult struct {
- Successful bool `json:"successful" yaml:"successful"`
- Skipped bool `json:"skipped" yaml:"skipped"`
- ResourceId string `json:"resource-id" yaml:"resource-id"`
- ResourceType string `json:"resource-type" yaml:"resource-type"`
- Title string `json:"title" yaml:"title"`
- Meta meta `json:"meta" yaml:"meta"`
- TestType int `json:"test-type" yaml:"test-type"`
- Result int `json:"result" yaml:"result"`
- Property string `json:"property" yaml:"property"`
- Err *ValidateError `json:"err" yaml:"err"`
- Expected []string `json:"expected" yaml:"expected"`
- Found []string `json:"found" yaml:"found"`
- Human string `json:"human" yaml:"human"`
- Duration time.Duration `json:"duration" yaml:"duration"`
+ Successful bool `json:"successful" yaml:"successful"`
+ Skipped bool `json:"skipped" yaml:"skipped"`
+ // Resource data
+ ResourceId string `json:"resource-id" yaml:"resource-id"`
+ ResourceType string `json:"resource-type" yaml:"resource-type"`
+ Property string `json:"property" yaml:"property"`
+
+ // User added info
+ Title string `json:"title" yaml:"title"`
+ Meta meta `json:"meta" yaml:"meta"`
+
+ // Result
+ Result int `json:"result" yaml:"result"`
+ Err *ValidateError `json:"err" yaml:"err"`
+ MatcherResult matchers.MatcherResult `json:"matcher-result" yaml:"matcher-result"`
+ StartTime time.Time `json:"start-time" yaml:"start-time"`
+ EndTime time.Time `json:"end-time" yaml:"end-time"`
+ Duration time.Duration `json:"duration" yaml:"duration"`
}
// ToOutcome converts the enum to a human-friendly string.
@@ -90,22 +91,46 @@ func (tr TestResult) ToOutcome() string {
}
}
-func skipResult(typeS string, testType int, id string, title string, meta meta, property string, startTime time.Time) TestResult {
+func (t TestResult) SortKey() string {
+ return fmt.Sprintf("%s:%s", t.ResourceType, t.ResourceId)
+}
+
+func skipResult(typeS string, id string, title string, meta meta, property string, startTime time.Time) TestResult {
+ endTime := time.Now()
return TestResult{
- Successful: true,
Result: SKIP,
Skipped: true,
ResourceType: typeS,
- TestType: testType,
ResourceId: id,
Title: title,
Meta: meta,
Property: property,
- Duration: startTime.Sub(startTime),
+ StartTime: startTime,
+ EndTime: endTime,
+ Duration: endTime.Sub(startTime),
}
}
func ValidateValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult {
+ if f, ok := actual.(func() (io.Reader, error)); ok {
+ if _, ok := expectedValue.([]any); !ok {
+ actual = func() (string, error) {
+ v, err := f()
+ if err != nil {
+ return "", err
+ }
+ i, err := matchers.ReaderToString{}.Transform(v)
+ if err != nil {
+ return "", err
+ }
+ return i.(string), nil
+ }
+ }
+ }
+ return ValidateGomegaValue(res, property, expectedValue, actual, skip)
+}
+
+func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult {
id := res.ID()
title := res.GetTitle()
meta := res.GetMeta()
@@ -115,7 +140,6 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual
if skip {
return skipResult(
typeS,
- Values,
id,
title,
meta,
@@ -125,6 +149,7 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual
}
var foundValue any
+ var gomegaMatcher matchers.GossMatcher
var err error
switch f := actual.(type) {
case func() (bool, error):
@@ -137,295 +162,62 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual
foundValue, err = f()
case func() (any, error):
foundValue, err = f()
+ case func() (io.Reader, error):
+ foundValue, err = f()
+ gomegaMatcher = matchers.HavePatterns(expectedValue)
default:
err = fmt.Errorf("Unknown method signature: %t", f)
}
+ foundValue = sanitizeExpectedValue(foundValue)
expectedValue = sanitizeExpectedValue(expectedValue)
- var gomegaMatcher types.GomegaMatcher
var success bool
- if err == nil {
+ if gomegaMatcher == nil && err == nil {
gomegaMatcher, err = matcherToGomegaMatcher(expectedValue)
}
- if err == nil {
- success, err = gomegaMatcher.Match(foundValue)
- }
- if err != nil {
- return TestResult{
- Successful: false,
- Result: FAIL,
- ResourceType: typeS,
- TestType: Values,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Err: toValidateError(err),
- Duration: time.Now().Sub(startTime),
- }
- }
-
- var failMessage string
- var result int
- if !success {
- failMessage = gomegaMatcher.FailureMessage(foundValue)
- result = FAIL
- }
-
- expected, _ := json.Marshal(expectedValue)
- found, _ := json.Marshal(foundValue)
-
- return TestResult{
- Successful: success,
- Result: result,
- ResourceType: typeS,
- TestType: Value,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Expected: []string{string(expected)},
- Found: []string{string(found)},
- Human: failMessage,
- Duration: time.Now().Sub(startTime),
- }
-}
-
-type patternMatcher interface {
- Match(string) bool
- Pattern() string
- Inverse() bool
-}
-
-type stringPattern struct {
- pattern string
- cleanPattern string
- inverse bool
-}
-
-func newStringPattern(str string) *stringPattern {
- var inverse bool
- if strings.HasPrefix(str, "!") {
- inverse = true
- }
- cleanPattern := strings.TrimLeft(str, "\\/!")
- return &stringPattern{
- pattern: str,
- cleanPattern: cleanPattern,
- inverse: inverse,
- }
-}
-
-func (s *stringPattern) Match(str string) bool {
- return strings.Contains(str, s.cleanPattern)
-}
-
-func (s *stringPattern) Pattern() string { return s.pattern }
-func (s *stringPattern) Inverse() bool { return s.inverse }
-
-type regexPattern struct {
- pattern string
- re *regexp.Regexp
- inverse bool
-}
-
-func newRegexPattern(str string) (*regexPattern, error) {
- var inverse bool
- cleanStr := str
- if strings.HasPrefix(str, "!") {
- inverse = true
- cleanStr = cleanStr[1:]
- }
- trimLeft := []rune{'\\', '/'}
- for _, r := range trimLeft {
- if rune(cleanStr[0]) == r {
- cleanStr = cleanStr[1:]
- break
- }
- }
- trimRight := []rune{'/'}
- for _, r := range trimRight {
- if rune(cleanStr[len(cleanStr)-1]) == r {
- cleanStr = cleanStr[:len(cleanStr)-1]
- break
- }
- }
-
- re, err := regexp.Compile(cleanStr)
-
- return ®exPattern{
- pattern: str,
- re: re,
- inverse: inverse,
- }, err
-
-}
-
-func (re *regexPattern) Match(str string) bool {
- return re.re.MatchString(str)
-}
-
-func (re *regexPattern) Pattern() string { return re.pattern }
-func (re *regexPattern) Inverse() bool { return re.inverse }
-
-func sliceToPatterns(slice []string) ([]patternMatcher, error) {
- var patterns []patternMatcher
- for _, s := range slice {
- if (strings.HasPrefix(s, "/") || strings.HasPrefix(s, "!/")) && strings.HasSuffix(s, "/") {
- pat, err := newRegexPattern(s)
- if err != nil {
- return nil, err
- }
- patterns = append(patterns, pat)
- } else {
- patterns = append(patterns, newStringPattern(s))
- }
- }
- return patterns, nil
-}
-
-func patternsToSlice(patterns []patternMatcher) []string {
- var slice []string
- for _, p := range patterns {
- slice = append(slice, p.Pattern())
- }
- return slice
-}
-
-func ValidateContains(res ResourceRead, property string, expectedValues []string, method func() (io.Reader, error), skip bool) TestResult {
- id := res.ID()
- title := res.GetTitle()
- meta := res.GetMeta()
- typ := reflect.TypeOf(res)
- typeS := strings.Split(typ.String(), ".")[1]
- startTime := time.Now()
- if skip {
- return skipResult(
- typeS,
- Values,
- id,
- title,
- meta,
- property,
- startTime,
- )
- }
- var err error
- var fh io.Reader
- var notfound []patternMatcher
- notfound, err = sliceToPatterns(expectedValues)
- // short circuit
- if len(notfound) == 0 && err == nil {
- return TestResult{
- Successful: true,
- Result: SUCCESS,
- ResourceType: typeS,
- TestType: Contains,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Expected: expectedValues,
- Duration: time.Now().Sub(startTime),
- }
- }
- if err == nil {
- fh, err = method()
- }
if err != nil {
+ endTime := time.Now()
return TestResult{
- Successful: false,
Result: FAIL,
ResourceType: typeS,
- TestType: Contains,
ResourceId: id,
Title: title,
Meta: meta,
Property: property,
Err: toValidateError(err),
- Duration: time.Now().Sub(startTime),
+ StartTime: startTime,
+ EndTime: endTime,
+ Duration: endTime.Sub(startTime),
}
}
- defer func() {
- // Do we need to close the stream?
- if rc, ok := fh.(io.ReadCloser); ok {
- rc.Close()
- }
- }()
-
- scanner := bufio.NewScanner(fh)
- scanner.Buffer(nil, maxScanTokenSize)
- var found []patternMatcher
- for scanner.Scan() {
- line := scanner.Text()
+ success, err = gomegaMatcher.Match(foundValue)
- i := 0
- for _, pat := range notfound {
- if pat.Match(line) {
- // Found it, but wasn't supposed to, don't mark it as found, but remove it from search
- if !pat.Inverse() {
- found = append(found, pat)
- }
- continue
- }
- notfound[i] = pat
- i++
- }
- notfound = notfound[:i]
- if len(notfound) == 0 {
- break
- }
- }
- if err := scanner.Err(); err != nil {
- return TestResult{
- Successful: false,
- Result: FAIL,
- ResourceType: typeS,
- TestType: Contains,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Err: toValidateError(err),
- Duration: time.Now().Sub(startTime),
- }
- }
-
- for _, pat := range notfound {
- // Didn't find it, but we didn't want to.. so we mark it as found
- // Empty pattern should match even if input to scanner is empty
- if pat.Inverse() || pat.Pattern() == "" {
- found = append(found, pat)
+ var matcherResult matchers.MatcherResult
+ result := SUCCESS
+ if success {
+ matcherResult = matchers.MatcherResult{
+ Actual: foundValue,
+ Message: "matches expectation",
+ Expected: expectedValue,
}
+ } else {
+ matcherResult = gomegaMatcher.FailureResult(foundValue)
+ result = FAIL
}
- if len(expectedValues) != len(found) {
- return TestResult{
- Successful: false,
- Result: FAIL,
- ResourceType: typeS,
- TestType: Contains,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Expected: expectedValues,
- Found: patternsToSlice(found),
- Duration: time.Now().Sub(startTime),
- }
- }
+ endTime := time.Now()
return TestResult{
- Successful: true,
- Result: SUCCESS,
- ResourceType: typeS,
- TestType: Contains,
- ResourceId: id,
- Title: title,
- Meta: meta,
- Property: property,
- Expected: expectedValues,
- Found: patternsToSlice(found),
- Duration: time.Now().Sub(startTime),
+ Result: result,
+ ResourceType: typeS,
+ ResourceId: id,
+ Title: title,
+ Meta: meta,
+ Property: property,
+ MatcherResult: matcherResult,
+ Err: toValidateError(err),
+ StartTime: startTime,
+ EndTime: endTime,
+ Duration: endTime.Sub(startTime),
}
}
diff --git a/resource/validate_test.go b/resource/validate_test.go
index 998f4431d..55a450205 100644
--- a/resource/validate_test.go
+++ b/resource/validate_test.go
@@ -21,13 +21,13 @@ func (f *FakeResource) GetMeta() meta { return meta{"foo": "bar"} }
var stringTests = []struct {
in, in2 any
- want bool
+ want int
}{
- {"", "", true},
- {"foo", "foo", true},
- {"foo", "bar", false},
- {"foo", "", false},
- {true, true, true},
+ {"", "", SUCCESS},
+ {"foo", "foo", SUCCESS},
+ {"foo", "bar", FAIL},
+ {"foo", "", FAIL},
+ {true, true, SUCCESS},
}
func TestValidateValue(t *testing.T) {
@@ -36,8 +36,8 @@ func TestValidateValue(t *testing.T) {
return c.in2, nil
}
got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
- if got.Successful != c.want {
- t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want)
+ if got.Result != c.want {
+ t.Errorf("%+v: got %v, want %v", c, got.Result, c.want)
}
}
}
@@ -48,8 +48,8 @@ func TestValidateValueErr(t *testing.T) {
return c.in2, fmt.Errorf("some err")
}
got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
- if got.Successful != false {
- t.Errorf("%+v: got %v, want %v", c, got.Successful, false)
+ if got.Result != FAIL {
+ t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL)
}
}
}
@@ -76,19 +76,19 @@ func BenchmarkValidateValue(b *testing.B) {
}
var containsTests = []struct {
- in []string
+ in []interface{}
in2 string
- want bool
+ want int
}{
- {[]string{""}, "", true},
- {[]string{"foo"}, "foo\nbar", true},
- {[]string{"!foo"}, "foo\nbar", false},
- {[]string{"!moo"}, "foo\nbar", true},
- {[]string{"/fo.*/"}, "foo\nbar", true},
- {[]string{"!/fo.*/"}, "foo\nbar", false},
- {[]string{"!/mo.*/"}, "foo\nbar", true},
- {[]string{"foo"}, "", false},
- {[]string{`/\s/tmp\b/`}, "test /tmp bar", true},
+ {[]interface{}{""}, "", SUCCESS},
+ {[]interface{}{"foo"}, "foo\nbar", SUCCESS},
+ {[]interface{}{"!foo"}, "foo\nbar", FAIL},
+ {[]interface{}{"!moo"}, "foo\nbar", SUCCESS},
+ {[]interface{}{"/fo.*/"}, "foo\nbar", SUCCESS},
+ {[]interface{}{"!/fo.*/"}, "foo\nbar", FAIL},
+ {[]interface{}{"!/mo.*/"}, "foo\nbar", SUCCESS},
+ {[]interface{}{"foo"}, "", FAIL},
+ {[]interface{}{`/\s/tmp\b/`}, "test /tmp bar", SUCCESS},
}
func TestValidateContains(t *testing.T) {
@@ -97,9 +97,9 @@ func TestValidateContains(t *testing.T) {
reader := strings.NewReader(c.in2)
return reader, nil
}
- got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false)
- if got.Successful != c.want {
- t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want)
+ got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
+ if got.Result != c.want {
+ t.Errorf("%+v: got %v, want %v", c, got.Result, c.want)
}
}
}
@@ -110,9 +110,9 @@ func TestValidateContainsErr(t *testing.T) {
reader := strings.NewReader(c.in2)
return reader, fmt.Errorf("some err")
}
- got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false)
- if got.Successful != false {
- t.Errorf("%+v: got %v, want %v", c, got.Successful, false)
+ got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
+ if got.Result != FAIL {
+ t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL)
}
}
}
@@ -122,7 +122,7 @@ func TestValidateContainsBadRegexErr(t *testing.T) {
reader := strings.NewReader("dummy")
return reader, nil
}
- got := ValidateContains(&FakeResource{""}, "", []string{"/*\\.* @@.*/"}, inFunc, false)
+ got := ValidateValue(&FakeResource{""}, "", []interface{}{"/*\\.* @@.*/"}, inFunc, false)
if got.Err == nil {
t.Errorf("Expected bad regex to raise error, got nil")
}
@@ -134,7 +134,7 @@ func TestValidateContainsSkip(t *testing.T) {
reader := strings.NewReader(c.in2)
return reader, nil
}
- got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, true)
+ got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, true)
if got.Result != SKIP {
t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP)
}
@@ -145,7 +145,7 @@ func TestResultMarshaling(t *testing.T) {
inFunc := func() (io.Reader, error) {
return nil, fmt.Errorf("dummy error")
}
- res := ValidateContains(&FakeResource{}, "", []string{"x"}, inFunc, false)
+ res := ValidateValue(&FakeResource{}, "", []string{"x"}, inFunc, false)
if res.Err == nil {
t.Fatalf("Expected to receive an error")
}
diff --git a/serve.go b/serve.go
index ec4b1a5b4..3adb34270 100644
--- a/serve.go
+++ b/serve.go
@@ -96,36 +96,28 @@ func (h healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (h healthHandler) processAndEnsureCached(negotiatedContentType string, outputer outputs.Outputer) res {
- cacheKey := fmt.Sprintf("res:%s", negotiatedContentType)
+ var tra [][]resource.TestResult
+ cacheKey := "res"
tmp, found := h.cache.Get(cacheKey)
if found {
- log.Printf("[TRACE] Returning cached[%s] (1).", cacheKey)
- return tmp.(res)
- }
-
- h.gossMu.Lock()
- defer h.gossMu.Unlock()
- tmp, found = h.cache.Get(cacheKey)
- if found {
- log.Printf("[TRACE] Returning cached[%s] (2).", cacheKey)
- return tmp.(res)
+ log.Printf("[TRACE] Returning cached[%s].", cacheKey)
+ tra = tmp.([][]resource.TestResult)
+ } else {
+ log.Printf("Stale cache[%s], running tests", cacheKey)
+ h.sys = system.New(h.c.PackageManager)
+ tra = h.validate()
+ h.cache.SetDefault(cacheKey, tra)
}
-
- log.Printf("[TRACE] Stale cache[%s], running tests", cacheKey)
- resp := h.runValidate(outputer)
- h.cache.SetDefault(cacheKey, resp)
- return resp
+ trc := testResultArrayToChan(tra)
+ return h.output(trc, outputer)
}
-func (h healthHandler) runValidate(outputer outputs.Outputer) res {
- h.sys = system.New(h.c.PackageManager)
- iStartTime := time.Now()
- out := validate(h.sys, h.gossConfig, h.c.DisabledResourceTypes, h.maxConcurrent)
+func (h healthHandler) output(trc <-chan []resource.TestResult, outputer outputs.Outputer) res {
var b bytes.Buffer
outputConfig := util.OutputConfig{
FormatOptions: h.c.FormatOptions,
}
- exitCode := outputer.Output(&b, out, iStartTime, outputConfig)
+ exitCode := outputer.Output(&b, trc, outputConfig)
resp := res{
body: b,
}
@@ -136,6 +128,28 @@ func (h healthHandler) runValidate(outputer outputs.Outputer) res {
}
return resp
}
+func (h healthHandler) validate() [][]resource.TestResult {
+ h.sys = system.New(h.c.PackageManager)
+ res := make([][]resource.TestResult, 0)
+ tr := validate(h.sys, h.gossConfig, h.c.DisabledResourceTypes, h.maxConcurrent)
+ for i := range tr {
+ res = append(res, i)
+ }
+ return res
+}
+
+func testResultArrayToChan(tra [][]resource.TestResult) <-chan []resource.TestResult {
+ c := make(chan []resource.TestResult)
+ go func(c chan []resource.TestResult) {
+ defer close(c)
+
+ for _, i := range tra {
+ c <- i
+ }
+ }(c)
+
+ return c
+}
const (
// https://en.wikipedia.org/wiki/Media_type
@@ -174,12 +188,3 @@ func (h healthHandler) responseContentType(outputName string) string {
}
return fmt.Sprintf("%s%s", mediaTypePrefix, outputName)
}
-
-func (h healthHandler) renderBody(results <-chan []resource.TestResult, outputer outputs.Outputer) (int, bytes.Buffer) {
- outputConfig := util.OutputConfig{
- FormatOptions: h.c.FormatOptions,
- }
- var b bytes.Buffer
- exitCode := outputer.Output(&b, results, time.Now(), outputConfig)
- return exitCode, b
-}
diff --git a/serve_test.go b/serve_test.go
index a6138f93b..b29fe3baf 100644
--- a/serve_test.go
+++ b/serve_test.go
@@ -268,14 +268,14 @@ func TestServeCacheNegotiatingContent(t *testing.T) {
logOutput.Reset()
})
- t.Run("immediately re-request but different accept header, cache should be cold", func(t *testing.T) {
+ t.Run("immediately re-request but different accept header, cache should be warm", func(t *testing.T) {
req := makeRequest(t, config, map[string][]string{
"accept": {"application/vnd.goss-rspecish"},
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
- assert.Contains(t, logOutput.String(), "Stale cache")
+ assert.NotContains(t, logOutput.String(), "Stale cache")
t.Log(logOutput.String())
logOutput.Reset()
})
diff --git a/store.go b/store.go
index abd8f10b8..0f4e560b6 100644
--- a/store.go
+++ b/store.go
@@ -76,12 +76,12 @@ func (t *TmplVars) Env() map[string]string {
func loadVars(varsFile string, varsInline string) (map[string]any, error) {
vars, err := varsFromFile(varsFile)
if err != nil {
- return nil, fmt.Errorf("Error: loading vars file '%s'\n%w", varsFile, err)
+ return nil, fmt.Errorf("loading vars file '%s'\n%w", varsFile, err)
}
varsExtra, err := varsFromString(varsInline)
if err != nil {
- return nil, fmt.Errorf("Error: loading inline vars\n%w", err)
+ return nil, fmt.Errorf("loading inline vars\n%w", err)
}
for k, v := range varsExtra {
@@ -142,7 +142,7 @@ func ReadJSONData(data []byte, detectFormat bool) (GossConfig, error) {
}
format := outStoreFormat
- if detectFormat == true {
+ if detectFormat {
format, err = getStoreFormatFromData(data)
if err != nil {
return GossConfig{}, err
@@ -314,10 +314,5 @@ func marshalYAML(gossConfig any) ([]byte, error) {
}
func unmarshalYAML(data []byte, v any) error {
- err := yaml.Unmarshal(data, v)
- if err != nil {
- return fmt.Errorf("could not unmarshal %q as YAML data: %s", string(data), err)
- }
-
- return nil
+ return yaml.Unmarshal(data, v)
}
diff --git a/system/file.go b/system/file.go
index bb2a85ba9..728194f98 100644
--- a/system/file.go
+++ b/system/file.go
@@ -19,7 +19,7 @@ import (
type File interface {
Path() string
Exists() (bool, error)
- Contains() (io.Reader, error)
+ Contents() (io.Reader, error)
Mode() (string, error)
Size() (int, error)
Filetype() (string, error)
@@ -83,7 +83,7 @@ func (f *DefFile) Exists() (bool, error) {
return true, err
}
-func (f *DefFile) Contains() (io.Reader, error) {
+func (f *DefFile) Contents() (io.Reader, error) {
if err := f.setup(); err != nil {
return nil, err
}
diff --git a/system/http.go b/system/http.go
index d821a1eeb..51c669dde 100644
--- a/system/http.go
+++ b/system/http.go
@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"os"
+ "sort"
"strings"
"time"
@@ -72,6 +73,7 @@ func HeaderToArray(header http.Header) (res []string) {
res = append(res, fmt.Sprintf("%s: %s", name, value))
}
}
+ sort.Strings(res)
return
}
diff --git a/system/mount.go b/system/mount.go
index bbdfe58b8..557dcbbcd 100644
--- a/system/mount.go
+++ b/system/mount.go
@@ -6,6 +6,7 @@ import (
"github.com/goss-org/goss/util"
"github.com/moby/sys/mountinfo"
+ "github.com/samber/lo"
)
type Mount interface {
@@ -77,8 +78,9 @@ func (m *DefMount) Opts() ([]string, error) {
if err := m.setup(); err != nil {
return nil, err
}
+ allOpts := splitMountInfo(strings.Join([]string{m.mountInfo.Options, m.mountInfo.VFSOptions}, ","))
- return strings.Split(m.mountInfo.Options, ","), nil
+ return lo.Uniq(allOpts), nil
}
func (m *DefMount) Source() (string, error) {
@@ -106,20 +108,22 @@ func (m *DefMount) Usage() (int, error) {
}
func getMount(mountpoint string) (*mountinfo.Info, error) {
- entries, err := mountinfo.GetMounts(func(e *mountinfo.Info) (skip bool, stop bool) {
- if e.Mountpoint == mountpoint {
- return false, true
- }
-
- return true, false
- })
+ entries, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mountpoint))
if err != nil {
return nil, err
}
-
if len(entries) == 0 {
return nil, fmt.Errorf("Mountpoint not found")
}
-
return entries[0], nil
}
+
+func splitMountInfo(s string) []string {
+ quoted := false
+ return strings.FieldsFunc(s, func(r rune) bool {
+ if r == '"' {
+ quoted = !quoted
+ }
+ return !quoted && r == ','
+ })
+}
diff --git a/system/mount_test.go b/system/mount_test.go
new file mode 100644
index 000000000..b9835c555
--- /dev/null
+++ b/system/mount_test.go
@@ -0,0 +1,21 @@
+package system
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestSplitMountInfo(t *testing.T) {
+ in := "rw,context=\"system_u:object_r:container_file_t:s0:c174,c741\",size=65536k,mode=755"
+ want := []string{
+ "rw",
+ "context=\"system_u:object_r:container_file_t:s0:c174,c741\"",
+ "size=65536k",
+ "mode=755",
+ }
+
+ got := splitMountInfo(in)
+
+ assert.DeepEqual(t, got, want)
+}
diff --git a/system/package_rpm.go b/system/package_rpm.go
index 4303573e7..9d6519f33 100644
--- a/system/package_rpm.go
+++ b/system/package_rpm.go
@@ -23,7 +23,7 @@ func (p *RpmPackage) setup() {
return
}
p.loaded = true
- cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%{VERSION}\n", p.name)
+ cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n", p.name)
if err := cmd.Run(); err != nil {
return
}
diff --git a/system/service.go b/system/service.go
index 79cbdcf30..df2a43dc7 100644
--- a/system/service.go
+++ b/system/service.go
@@ -7,6 +7,7 @@ type Service interface {
Exists() (bool, error)
Enabled() (bool, error)
Running() (bool, error)
+ RunLevels() ([]string, error)
}
func invalidService(s string) bool {
diff --git a/system/service_init.go b/system/service_init.go
index 56f8a14e5..590e43f26 100644
--- a/system/service_init.go
+++ b/system/service_init.go
@@ -4,13 +4,15 @@ import (
"fmt"
"os"
"path/filepath"
+ "regexp"
"github.com/goss-org/goss/util"
)
type ServiceInit struct {
- service string
- alpine bool
+ service string
+ alpine bool
+ runlevel string
}
func NewServiceInit(service string, system *System, config util.Config) Service {
@@ -18,7 +20,11 @@ func NewServiceInit(service string, system *System, config util.Config) Service
}
func NewAlpineServiceInit(service string, system *System, config util.Config) Service {
- return &ServiceInit{service: service, alpine: true}
+ runlevel := config.RunLevel
+ if runlevel == "" {
+ runlevel = "sysinit"
+ }
+ return &ServiceInit{service: service, alpine: true, runlevel: runlevel}
}
func (s *ServiceInit) Service() string {
@@ -39,10 +45,24 @@ func (s *ServiceInit) Enabled() (bool, error) {
if invalidService(s.service) {
return false, nil
}
+ var runLevels []string
+ var err error
+ if s.alpine {
+ runLevels, err = alpineServiceRunLevels(s.service)
+ } else {
+ runLevels, err = initServiceRunLevels(s.service)
+ }
+ return len(runLevels) != 0, err
+}
+
+func (s *ServiceInit) RunLevels() ([]string, error) {
+ if invalidService(s.service) {
+ return nil, nil
+ }
if s.alpine {
- return alpineInitServiceEnabled(s.service, "sysinit")
+ return alpineServiceRunLevels(s.service)
} else {
- return initServiceEnabled(s.service, 3)
+ return initServiceRunLevels(s.service)
}
}
@@ -58,18 +78,34 @@ func (s *ServiceInit) Running() (bool, error) {
return false, nil
}
-func initServiceEnabled(service string, level int) (bool, error) {
- matches, err := filepath.Glob(fmt.Sprintf("/etc/rc%d.d/S[0-9][0-9]%s", level, service))
- if err == nil && matches != nil {
- return true, nil
+func initServiceRunLevels(service string) ([]string, error) {
+ var runLevels []string
+ matches, err := filepath.Glob(fmt.Sprintf("/etc/rc*.d/S[0-9][0-9]%s", service))
+ if err != nil {
+ return nil, err
}
- return false, err
+ re := regexp.MustCompile("/etc/rc([0-9]+).d/")
+ for _, m := range matches {
+ matches := re.FindStringSubmatch(m)
+ if matches != nil {
+ runLevels = append(runLevels, matches[1])
+ }
+ }
+ return runLevels, nil
}
-func alpineInitServiceEnabled(service string, level string) (bool, error) {
- matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/%s/%s", level, service))
- if err == nil && matches != nil {
- return true, nil
+func alpineServiceRunLevels(service string) ([]string, error) {
+ var runLevels []string
+ matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/*/%s", service))
+ if err != nil {
+ return nil, err
+ }
+ re := regexp.MustCompile("/etc/runlevels/([^/]+)")
+ for _, m := range matches {
+ matches := re.FindStringSubmatch(m)
+ if matches != nil {
+ runLevels = append(runLevels, matches[1])
+ }
}
- return false, err
+ return runLevels, nil
}
diff --git a/system/service_systemd.go b/system/service_systemd.go
index 6e48322e5..4a950500e 100644
--- a/system/service_systemd.go
+++ b/system/service_systemd.go
@@ -85,3 +85,7 @@ func (s *ServiceSystemd) Running() (bool, error) {
}
return false, nil
}
+
+func (s *ServiceSystemd) RunLevels() ([]string, error) {
+ return nil, nil
+}
diff --git a/system/service_upstart.go b/system/service_upstart.go
index ab60590a9..e5a9dcb23 100644
--- a/system/service_upstart.go
+++ b/system/service_upstart.go
@@ -77,3 +77,7 @@ func (s *ServiceUpstart) Running() (bool, error) {
}
return false, nil
}
+func (s *ServiceUpstart) RunLevels() ([]string, error) {
+ sysv := &ServiceInit{service: s.service}
+ return sysv.RunLevels()
+}
diff --git a/system/system.go b/system/system.go
index 7ab5a0eb0..dfef39859 100644
--- a/system/system.go
+++ b/system/system.go
@@ -8,6 +8,7 @@ import (
"sync"
"github.com/goss-org/GOnetstat"
+ // This needs a better name
"github.com/goss-org/go-ps"
util2 "github.com/goss-org/goss/util"
diff --git a/testdata/matching_basic.yaml b/testdata/matching_basic.yaml
new file mode 100644
index 000000000..edd9ee672
--- /dev/null
+++ b/testdata/matching_basic.yaml
@@ -0,0 +1,114 @@
+matching:
+ # Basic matchers
+ basic_string:
+ content: 'this is a test'
+ matches: 'this is a test'
+
+ basic_string_regexp:
+ content: 'this is a test'
+ matches:
+ match-regexp: '^this'
+
+ basic_string_skip:
+ skip: true
+ content: 'this is a test'
+ matches: 'this is a test'
+
+ basic_semver:
+ content: '1.2.3'
+ matches:
+ semver-constraint: '>=1.2.0'
+
+ basic_int:
+ content: 42
+ matches: 42
+
+ basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ - 'group1'
+ - 'group2'
+
+ basic_array_matchers:
+ content: [foo, bar, moo]
+ matches:
+ and:
+ - contain-elements: [foo, bar]
+ - [foo, bar] # same as above
+ - equal: [foo, bar, moo] # order matters, exact match
+ - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
+ - contain-element:
+ have-prefix: b
+ - contain-element:
+ have-suffix: r
+
+ basic_reader:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ - 'foo'
+ - '/^m.*w$/'
+ - '!ftw'
+ - '!/^ERROR:/'
+
+ # Negated
+ negated_basic_string:
+ content: 'this is a test'
+ matches:
+ not: 'this is a failing test'
+
+ negated_basic_string_regexp:
+ content: 'this is a test'
+ matches:
+ not:
+ match-regexp: '^foo'
+
+ negated_basic_int:
+ content: 42
+ matches:
+ not: 43
+
+ negated_basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ not:
+ - 'group1'
+ - 'group2'
+ - 'group2'
+ - 'group4'
+
+ negated_basic_array_matchers:
+ content: [foo, bar, moo]
+ matches:
+ and:
+ - not:
+ contain-elements: [fox, box]
+ - not: [fox, bax] # same as above
+ - not:
+ equal: [fox, bax, mox] # order matters, exact match
+ - not:
+ consist-of: [have-suffix: x, have-prefix: t, box] # order doesn't matter, can use matchers
+ - not:
+ contain-element:
+ have-prefix: x
+
+ negated_basic_reader:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ not:
+ contain-elements:
+ - 'fox'
+ - '/^t.*w$/'
+ - '!foo'
+ - '!/^foo/'
diff --git a/testdata/matching_basic_failing.yaml b/testdata/matching_basic_failing.yaml
new file mode 100644
index 000000000..51a6a4277
--- /dev/null
+++ b/testdata/matching_basic_failing.yaml
@@ -0,0 +1,181 @@
+matching:
+ # Basic matchers
+ basic_string:
+ content: 'this is a test'
+ matches: 'this is a failing test'
+
+ basic_string_have_prefix:
+ content: 'foo'
+ matches:
+ have-prefix: 'g'
+
+ basic_string_have_suffix:
+ content: 'foo'
+ matches:
+ have-suffix: 'x'
+
+ basic_string_contain_substring:
+ content: 'foo'
+ matches:
+ contain-substring: 'x'
+
+ basic_string_regexp:
+ content: 'this is a test'
+ matches:
+ match-regexp: '^foo'
+
+ basic_semver:
+ content: '1.2.3'
+ matches:
+ or:
+ - semver-constraint: '>=9.9.0'
+
+ basic_len:
+ content: "123"
+ matches:
+ or:
+ - have-len: 2
+
+ basic_int:
+ content: 42
+ matches: 43
+
+ basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ - 'group1'
+ - 'group2'
+ - 'group2'
+ - 'group4'
+
+ basic_array_matchers:
+ content: [foo, bar, moo]
+ matches:
+ or:
+ - contain-elements: [fox, box]
+ - [fox, bax] # same as above
+ - equal: [fox, bax, mox] # order matters, exact match
+ - consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers
+ - contain-element:
+ have-prefix: x
+ - contain-element:
+ have-suffix: x
+
+ basic_array_consists_of:
+ content: [foo, bar, moo]
+ matches:
+ consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers
+
+ basic_reader:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ - 'fox'
+ - '/^t.*w$/'
+ - '!foo'
+ - '!/^foo/'
+
+ # Negated
+ negated_basic_string:
+ content: 'this is a test'
+ matches:
+ not: 'this is a test'
+
+ negatedbasic_string_regexp:
+ content: 'this is a test'
+ matches:
+ not:
+ match-regexp: '^this'
+
+ negatedbasic_string_have_prefix:
+ content: 'foo'
+ matches:
+ not:
+ have-prefix: 'f'
+
+ negatedbasic_string_have_suffix:
+ content: 'foo'
+ matches:
+ not:
+ have-suffix: 'o'
+
+ negatedbasic_string_contain_substring:
+ content: 'foo'
+ matches:
+ not:
+ contain-substring: 'oo'
+
+ negatedbasic_len:
+ content: '123'
+ matches:
+ not:
+ have-len: 3
+
+ negated_basic_int:
+ content: 42
+ matches:
+ not: 42
+
+ negated_and:
+ content: 42
+ matches:
+ not:
+ and:
+ - 42
+ - 42
+
+ negated_basic_array:
+ content:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+ matches:
+ not:
+ - 'group1'
+ - 'group2'
+ - 'group3'
+
+ negated_basic_array_matchers:
+ content: [foo, bar, moo]
+ matches:
+ or:
+ - not:
+ contain-elements: [foo, bar]
+ - not:
+ [foo, bar] # same as above
+ - not:
+ equal: [foo, bar, moo] # order matters, exact match
+ - not:
+ consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
+ - not:
+ contain-element:
+ have-prefix: b
+
+ negated_basic_array_contain_element:
+ content: [foo, bar, moo]
+ matches:
+ not:
+ contain-element: foo
+
+ negated_basic_array_consists_of:
+ content: [foo, bar, moo]
+ matches:
+ not:
+ consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
+
+ negated_basic_reader:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ not:
+ - 'foo'
+ - '/^m.*w$/'
+ - '!ftw'
+ - '!/^ERROR:/'
diff --git a/testdata/matching_transformers.yaml b/testdata/matching_transformers.yaml
new file mode 100644
index 000000000..e30100714
--- /dev/null
+++ b/testdata/matching_transformers.yaml
@@ -0,0 +1,129 @@
+matching:
+ basic_reader_as_array:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ and:
+ - contain-element: {contain-substring: 'foo'}
+ - contain-element: {match-regexp: '^m.*w$'}
+ - not: {contain-substring: 'ftw'}
+ - not: {match-regexp: '^ERROR:'}
+
+ test_numeric_string:
+ content: 128
+ matches:
+ and:
+ - '128'
+ - have-prefix: '1'
+ - have-suffix: '8'
+ - match-regexp: '\d{3}'
+
+ test_string_numeric:
+ content: '128'
+ matches:
+ and:
+ - 128
+ - 128.0
+ - le: 128
+ - gt: 120
+
+ test_string_float:
+ content: '128.3'
+ matches:
+ and:
+ - 128.3
+ - le: 129
+ - gt: 120.2
+
+ test_array:
+ content:
+ - '45'
+ - '46'
+ - '47'
+ matches:
+ - contain-element: {match-regexp: "4."}
+ - '45'
+ - and: [{ge: 46}, {le: 50}]
+
+ test_reader_using_string_matchers:
+ content: |
+ foo bar
+ 15
+ moo cow
+ as-reader: true
+ matches:
+ and:
+ - have-len: 19
+ - |
+ foo bar
+ 15
+ moo cow
+ - have-prefix: 'foo'
+ - have-suffix: "cow\n"
+ - contain-element:
+ have-prefix: 'moo'
+ - contain-elements:
+ - not: 'this_doesnt_exist'
+ - lt: 20
+ - have-prefix: 'moo'
+
+ test_reader_using_array:
+ content: |
+ foo bar
+ 15
+ moo cow
+ as-reader: true
+ matches:
+ - "foo bar"
+ - "15"
+ - "moo cow"
+
+
+ test_reader_as_single_string:
+ content: 'cool'
+ as-reader: true
+ matches: 'cool'
+
+ test_reader_using_int_matchers:
+ content: '40'
+ as-reader: true
+ matches:
+ and:
+ - le: 250
+ - ge: 20
+
+
+ test_gjson_transform:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ moo.nested: cow
+ foo: {have-prefix: b}
+ count: {le: 25}
+ '@this': {have-key: "foo"}
+ moo:
+ and:
+ - have-key: "nested"
+ - {not: {have-key: "nested2"}}
+
+ test_gjson_using_this_and_equal:
+ content: '{"foo": "bar", "baz": "bing"}'
+ matches:
+ gjson:
+ '@this':
+ equal:
+ foo: bar
+ baz: bing
+
+ test_gjson_have_key_array:
+ content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
+ matches:
+ gjson:
+ arr:
+ # or tests MarshalJSON
+ or:
+ - contain-elements:
+ - have-key: 'nested'
diff --git a/testdata/matching_transformers_failing.yaml b/testdata/matching_transformers_failing.yaml
new file mode 100644
index 000000000..f49482ba0
--- /dev/null
+++ b/testdata/matching_transformers_failing.yaml
@@ -0,0 +1,162 @@
+matching:
+ basic_reader_as_array:
+ as-reader: true
+ content: |
+ foo bar
+ moo cow
+ matches:
+ and:
+ - contain-element: {contain-substring: 'fox'}
+ - contain-element: {match-regexp: '^t.*w$'}
+ - not: {contain-substring: 'foo'}
+ - not: {match-regexp: '^foo'}
+
+ test_numeric_string:
+ content: 128
+ matches:
+ and:
+ - '129'
+ - have-prefix: '2'
+ - have-suffix: '9'
+ - match-regexp: '\s{3}'
+
+ test_string_numeric:
+ content: '128'
+ matches:
+ and:
+ - 129
+ - 129.1
+ - le: 127
+ - gt: 130
+
+ test_string_float:
+ content: '128.3'
+ matches:
+ and:
+ - 129.3
+ - le: 127
+ - gt: 130.2
+
+ test_array:
+ content:
+ - '45'
+ - '46'
+ - '47'
+ matches:
+ - contain-element: {match-regexp: "5."}
+ - '55'
+ - and: [{ge: 56}, {le: 30}]
+
+ test_reader_using_string_matchers:
+ content: |
+ foo bar
+ 15
+ moo cow
+ as-reader: true
+ matches:
+ and:
+ - have-len: 15
+ - |
+ fox bar
+ 15
+ moo cow
+ - have-prefix: 'fox'
+ - have-suffix: "tow\n"
+ - contain-element:
+ have-prefix: 'too'
+ - contain-elements:
+ - not: 'moo cow'
+ - lt: 10
+ - have-prefix: 'tow'
+
+
+ test_reader_as_single_string:
+ content: 'cool'
+ as-reader: true
+ matches: 'not-cool'
+
+ test_reader_using_int_matchers:
+ content: '40'
+ as-reader: true
+ matches:
+ and:
+ - le: 20
+ - ge: 50
+
+
+ test_gjson_transform_simple:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ moo.nested: cowx
+
+ test_gjson_transform_nested_prefix:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ foo: {have-prefix: x}
+
+ test_gjson_transform_nested_count:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ count: {le: 10}
+
+ test_gjson_transform_nested_this:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ '@this': {have-key: "nope"}
+
+ test_gjson_transform_nested_and:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ moo:
+ and:
+ - {have-key: "nope"}
+ - {not: {have-key: "nested"}}
+
+ test_gjson_transform_not_key:
+ content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
+ as-reader: true
+ matches:
+ gjson:
+ moo:
+ not:
+ have-key: "nested"
+
+ test_gjson_using_this_and_equal:
+ content: '{"foo": "bar", "baz": "bing"}'
+ matches:
+ gjson:
+ '@this':
+ equal:
+ fox: bar
+ baz: bing
+
+ test_gjson_have_key_array:
+ content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
+ matches:
+ gjson:
+ '@this':
+ or:
+ - have-key: "fail"
+
+ test_gjson_not_found:
+ content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
+ matches:
+ gjson:
+ foo: 'bar'
+
+ test_gjson_invalid:
+ content: '{"arr"'
+ matches:
+ gjson:
+ '@this':
+ - have-key: "arr"
diff --git a/testdata/out_matching_basic.0.documentation b/testdata/out_matching_basic.0.documentation
new file mode 100644
index 000000000..d96a8e279
--- /dev/null
+++ b/testdata/out_matching_basic.0.documentation
@@ -0,0 +1,22 @@
+Matching: basic_array: matches: matches expectation: ["group1","group2"]
+Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]}
+Matching: basic_int: matches: matches expectation: 42
+Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"]
+Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"}
+Matching: basic_string: matches: matches expectation: "this is a test"
+Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"}
+Matching: basic_string_skip: matches: skipped
+Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]}
+Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]}
+Matching: negated_basic_int: matches: matches expectation: {"not":43}
+Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}}
+Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"}
+Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}}
+
+
+Failures/Skipped:
+
+Matching: basic_string_skip: matches: skipped
+
+Total Duration:
+Count: 14, Failed: 0, Skipped: 1
diff --git a/testdata/out_matching_basic.0.nagios b/testdata/out_matching_basic.0.nagios
new file mode 100644
index 000000000..fc2d28ea6
--- /dev/null
+++ b/testdata/out_matching_basic.0.nagios
@@ -0,0 +1 @@
+GOSS OK - Count: 14, Failed: 0, Skipped: 1, Duration:
diff --git a/testdata/out_matching_basic.0.rspecish b/testdata/out_matching_basic.0.rspecish
new file mode 100644
index 000000000..3d8077bbf
--- /dev/null
+++ b/testdata/out_matching_basic.0.rspecish
@@ -0,0 +1,8 @@
+.......S......
+
+Failures/Skipped:
+
+Matching: basic_string_skip: matches: skipped
+
+Total Duration:
+Count: 14, Failed: 0, Skipped: 1
diff --git a/testdata/out_matching_basic.0.tap b/testdata/out_matching_basic.0.tap
new file mode 100644
index 000000000..0ab691b7c
--- /dev/null
+++ b/testdata/out_matching_basic.0.tap
@@ -0,0 +1,15 @@
+1..14
+ok 1 - Matching: basic_array: matches: matches expectation: ["group1","group2"]
+ok 2 - Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]}
+ok 3 - Matching: basic_int: matches: matches expectation: 42
+ok 4 - Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"]
+ok 5 - Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"}
+ok 6 - Matching: basic_string: matches: matches expectation: "this is a test"
+ok 7 - Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"}
+ok 8 - # SKIP Matching: basic_string_skip: matches: skipped
+ok 9 - Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]}
+ok 10 - Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]}
+ok 11 - Matching: negated_basic_int: matches: matches expectation: {"not":43}
+ok 12 - Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}}
+ok 13 - Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"}
+ok 14 - Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}}
diff --git a/testdata/out_matching_basic_failing.1.documentation b/testdata/out_matching_basic_failing.1.documentation
new file mode 100644
index 000000000..3e6406f26
--- /dev/null
+++ b/testdata/out_matching_basic_failing.1.documentation
@@ -0,0 +1,299 @@
+Matching: basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+to contain elements
+ ["group1","group2","group2","group4"]
+the missing elements were
+ ["group2","group4"]
+Matching: basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+to consist of
+ ["fox",{"have-prefix":"t"},"box"]
+the missing elements were
+ ["fox",{"have-prefix":"t"},"box"]
+the extra elements were
+ ["foo","bar","moo"]
+Matching: basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
+Matching: basic_int: matches:
+Expected
+ 42
+to be numerically eq
+ 43
+Matching: basic_len: matches:
+Expected
+ "123"
+to satisfy at least one of these matchers
+ [{"have-len":2}]
+Matching: basic_reader: matches:
+Expected
+ "object: *strings.Reader"
+to have patterns
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+the missing elements were
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+Matching: basic_semver: matches:
+Expected
+ "1.2.3"
+to satisfy at least one of these matchers
+ [{"semver-constraint":">=9.9.0"}]
+Matching: basic_string: matches:
+Expected
+ "this is a test"
+to equal
+ "this is a failing test"
+Matching: basic_string_contain_substring: matches:
+Expected
+ "foo"
+to contain substring
+ "x"
+Matching: basic_string_have_prefix: matches:
+Expected
+ "foo"
+to have prefix
+ "g"
+Matching: basic_string_have_suffix: matches:
+Expected
+ "foo"
+to have suffix
+ "x"
+Matching: basic_string_regexp: matches:
+Expected
+ "this is a test"
+to match regular expression
+ "^foo"
+Matching: negated_and: matches:
+Expected
+ 42
+not to satisfy all of these matchers
+ [{"eq":42},{"eq":42}]
+Matching: negated_basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+not to contain elements
+ ["group1","group2","group3"]
+Matching: negated_basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+not to consist of
+ ["foo",{"have-prefix":"m"},"bar"]
+Matching: negated_basic_array_contain_element: matches:
+Expected
+ ["foo","bar","moo"]
+not to contain element matching
+ "foo"
+Matching: negated_basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
+Matching: negated_basic_int: matches:
+Expected
+ 42
+not to be numerically eq
+ 42
+Matching: negated_basic_reader: matches:
+Error
+ ContainElements matcher expects an array/slice/map. Got:
+ : foo bar
+ moo cow
+
+Matching: negated_basic_string: matches:
+Expected
+ "this is a test"
+not to equal
+ "this is a test"
+Matching: negatedbasic_len: matches:
+Expected
+ "123"
+not to have length
+ 3
+Matching: negatedbasic_string_contain_substring: matches:
+Expected
+ "foo"
+not to contain substring
+ "oo"
+Matching: negatedbasic_string_have_prefix: matches:
+Expected
+ "foo"
+not to have prefix
+ "f"
+Matching: negatedbasic_string_have_suffix: matches:
+Expected
+ "foo"
+not to have suffix
+ "o"
+Matching: negatedbasic_string_regexp: matches:
+Expected
+ "this is a test"
+not to match regular expression
+ "^this"
+
+
+Failures/Skipped:
+
+Matching: basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+to contain elements
+ ["group1","group2","group2","group4"]
+the missing elements were
+ ["group2","group4"]
+
+Matching: basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+to consist of
+ ["fox",{"have-prefix":"t"},"box"]
+the missing elements were
+ ["fox",{"have-prefix":"t"},"box"]
+the extra elements were
+ ["foo","bar","moo"]
+
+Matching: basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
+
+Matching: basic_int: matches:
+Expected
+ 42
+to be numerically eq
+ 43
+
+Matching: basic_len: matches:
+Expected
+ "123"
+to satisfy at least one of these matchers
+ [{"have-len":2}]
+
+Matching: basic_reader: matches:
+Expected
+ "object: *strings.Reader"
+to have patterns
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+the missing elements were
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+
+Matching: basic_semver: matches:
+Expected
+ "1.2.3"
+to satisfy at least one of these matchers
+ [{"semver-constraint":">=9.9.0"}]
+
+Matching: basic_string: matches:
+Expected
+ "this is a test"
+to equal
+ "this is a failing test"
+
+Matching: basic_string_contain_substring: matches:
+Expected
+ "foo"
+to contain substring
+ "x"
+
+Matching: basic_string_have_prefix: matches:
+Expected
+ "foo"
+to have prefix
+ "g"
+
+Matching: basic_string_have_suffix: matches:
+Expected
+ "foo"
+to have suffix
+ "x"
+
+Matching: basic_string_regexp: matches:
+Expected
+ "this is a test"
+to match regular expression
+ "^foo"
+
+Matching: negated_and: matches:
+Expected
+ 42
+not to satisfy all of these matchers
+ [{"eq":42},{"eq":42}]
+
+Matching: negated_basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+not to contain elements
+ ["group1","group2","group3"]
+
+Matching: negated_basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+not to consist of
+ ["foo",{"have-prefix":"m"},"bar"]
+
+Matching: negated_basic_array_contain_element: matches:
+Expected
+ ["foo","bar","moo"]
+not to contain element matching
+ "foo"
+
+Matching: negated_basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
+
+Matching: negated_basic_int: matches:
+Expected
+ 42
+not to be numerically eq
+ 42
+
+Matching: negated_basic_reader: matches:
+Error
+ ContainElements matcher expects an array/slice/map. Got:
+ : foo bar
+ moo cow
+
+
+Matching: negated_basic_string: matches:
+Expected
+ "this is a test"
+not to equal
+ "this is a test"
+
+Matching: negatedbasic_len: matches:
+Expected
+ "123"
+not to have length
+ 3
+
+Matching: negatedbasic_string_contain_substring: matches:
+Expected
+ "foo"
+not to contain substring
+ "oo"
+
+Matching: negatedbasic_string_have_prefix: matches:
+Expected
+ "foo"
+not to have prefix
+ "f"
+
+Matching: negatedbasic_string_have_suffix: matches:
+Expected
+ "foo"
+not to have suffix
+ "o"
+
+Matching: negatedbasic_string_regexp: matches:
+Expected
+ "this is a test"
+not to match regular expression
+ "^this"
+
+Total Duration:
+Count: 25, Failed: 25, Skipped: 0
diff --git a/testdata/out_matching_basic_failing.1.rspecish b/testdata/out_matching_basic_failing.1.rspecish
new file mode 100644
index 000000000..8618ec70c
--- /dev/null
+++ b/testdata/out_matching_basic_failing.1.rspecish
@@ -0,0 +1,165 @@
+FFFFFFFFFFFFFFFFFFFFFFFFF
+
+Failures/Skipped:
+
+Matching: basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+to contain elements
+ ["group1","group2","group2","group4"]
+the missing elements were
+ ["group2","group4"]
+
+Matching: basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+to consist of
+ ["fox",{"have-prefix":"t"},"box"]
+the missing elements were
+ ["fox",{"have-prefix":"t"},"box"]
+the extra elements were
+ ["foo","bar","moo"]
+
+Matching: basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
+
+Matching: basic_int: matches:
+Expected
+ 42
+to be numerically eq
+ 43
+
+Matching: basic_len: matches:
+Expected
+ "123"
+to satisfy at least one of these matchers
+ [{"have-len":2}]
+
+Matching: basic_reader: matches:
+Expected
+ "object: *strings.Reader"
+to have patterns
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+the missing elements were
+ ["fox","/^t.*w$/","!foo","!/^foo/"]
+
+Matching: basic_semver: matches:
+Expected
+ "1.2.3"
+to satisfy at least one of these matchers
+ [{"semver-constraint":">=9.9.0"}]
+
+Matching: basic_string: matches:
+Expected
+ "this is a test"
+to equal
+ "this is a failing test"
+
+Matching: basic_string_contain_substring: matches:
+Expected
+ "foo"
+to contain substring
+ "x"
+
+Matching: basic_string_have_prefix: matches:
+Expected
+ "foo"
+to have prefix
+ "g"
+
+Matching: basic_string_have_suffix: matches:
+Expected
+ "foo"
+to have suffix
+ "x"
+
+Matching: basic_string_regexp: matches:
+Expected
+ "this is a test"
+to match regular expression
+ "^foo"
+
+Matching: negated_and: matches:
+Expected
+ 42
+not to satisfy all of these matchers
+ [{"eq":42},{"eq":42}]
+
+Matching: negated_basic_array: matches:
+Expected
+ ["group1","group2","group3"]
+not to contain elements
+ ["group1","group2","group3"]
+
+Matching: negated_basic_array_consists_of: matches:
+Expected
+ ["foo","bar","moo"]
+not to consist of
+ ["foo",{"have-prefix":"m"},"bar"]
+
+Matching: negated_basic_array_contain_element: matches:
+Expected
+ ["foo","bar","moo"]
+not to contain element matching
+ "foo"
+
+Matching: negated_basic_array_matchers: matches:
+Expected
+ ["foo","bar","moo"]
+to satisfy at least one of these matchers
+ [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
+
+Matching: negated_basic_int: matches:
+Expected
+ 42
+not to be numerically eq
+ 42
+
+Matching: negated_basic_reader: matches:
+Error
+ ContainElements matcher expects an array/slice/map. Got:
+ : foo bar
+ moo cow
+
+
+Matching: negated_basic_string: matches:
+Expected
+ "this is a test"
+not to equal
+ "this is a test"
+
+Matching: negatedbasic_len: matches:
+Expected
+ "123"
+not to have length
+ 3
+
+Matching: negatedbasic_string_contain_substring: matches:
+Expected
+ "foo"
+not to contain substring
+ "oo"
+
+Matching: negatedbasic_string_have_prefix: matches:
+Expected
+ "foo"
+not to have prefix
+ "f"
+
+Matching: negatedbasic_string_have_suffix: matches:
+Expected
+ "foo"
+not to have suffix
+ "o"
+
+Matching: negatedbasic_string_regexp: matches:
+Expected
+ "this is a test"
+not to match regular expression
+ "^this"
+
+Total Duration:
+Count: 25, Failed: 25, Skipped: 0
diff --git a/testdata/out_matching_basic_failing.1.tap b/testdata/out_matching_basic_failing.1.tap
new file mode 100644
index 000000000..1e7b141ba
--- /dev/null
+++ b/testdata/out_matching_basic_failing.1.tap
@@ -0,0 +1,26 @@
+1..25
+not ok 1 - Matching: basic_array: matches: Expected ["group1","group2","group3"] to contain elements ["group1","group2","group2","group4"] the missing elements were ["group2","group4"]
+not ok 2 - Matching: basic_array_consists_of: matches: Expected ["foo","bar","moo"] to consist of ["fox",{"have-prefix":"t"},"box"] the missing elements were ["fox",{"have-prefix":"t"},"box"] the extra elements were ["foo","bar","moo"]
+not ok 3 - Matching: basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
+not ok 4 - Matching: basic_int: matches: Expected 42 to be numerically eq 43
+not ok 5 - Matching: basic_len: matches: Expected "123" to satisfy at least one of these matchers [{"have-len":2}]
+not ok 6 - Matching: basic_reader: matches: Expected "object: *strings.Reader" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"]
+not ok 7 - Matching: basic_semver: matches: Expected "1.2.3" to satisfy at least one of these matchers [{"semver-constraint":">=9.9.0"}]
+not ok 8 - Matching: basic_string: matches: Expected "this is a test" to equal "this is a failing test"
+not ok 9 - Matching: basic_string_contain_substring: matches: Expected "foo" to contain substring "x"
+not ok 10 - Matching: basic_string_have_prefix: matches: Expected "foo" to have prefix "g"
+not ok 11 - Matching: basic_string_have_suffix: matches: Expected "foo" to have suffix "x"
+not ok 12 - Matching: basic_string_regexp: matches: Expected "this is a test" to match regular expression "^foo"
+not ok 13 - Matching: negated_and: matches: Expected 42 not to satisfy all of these matchers [{"eq":42},{"eq":42}]
+not ok 14 - Matching: negated_basic_array: matches: Expected ["group1","group2","group3"] not to contain elements ["group1","group2","group3"]
+not ok 15 - Matching: negated_basic_array_consists_of: matches: Expected ["foo","bar","moo"] not to consist of ["foo",{"have-prefix":"m"},"bar"]
+not ok 16 - Matching: negated_basic_array_contain_element: matches: Expected ["foo","bar","moo"] not to contain element matching "foo"
+not ok 17 - Matching: negated_basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
+not ok 18 - Matching: negated_basic_int: matches: Expected 42 not to be numerically eq 42
+not ok 19 - Matching: negated_basic_reader: matches: Error ContainElements matcher expects an array/slice/map. Got: : foo bar moo cow
+not ok 20 - Matching: negated_basic_string: matches: Expected "this is a test" not to equal "this is a test"
+not ok 21 - Matching: negatedbasic_len: matches: Expected "123" not to have length 3
+not ok 22 - Matching: negatedbasic_string_contain_substring: matches: Expected "foo" not to contain substring "oo"
+not ok 23 - Matching: negatedbasic_string_have_prefix: matches: Expected "foo" not to have prefix "f"
+not ok 24 - Matching: negatedbasic_string_have_suffix: matches: Expected "foo" not to have suffix "o"
+not ok 25 - Matching: negatedbasic_string_regexp: matches: Expected "this is a test" not to match regular expression "^this"
diff --git a/testdata/out_matching_basic_failing.2.nagios b/testdata/out_matching_basic_failing.2.nagios
new file mode 100644
index 000000000..aa3ddac51
--- /dev/null
+++ b/testdata/out_matching_basic_failing.2.nagios
@@ -0,0 +1 @@
+GOSS CRITICAL - Count: 25, Failed: 25, Skipped: 0, Duration:
diff --git a/testdata/out_matching_transformers.0.documentation b/testdata/out_matching_transformers.0.documentation
new file mode 100644
index 000000000..136a6449a
--- /dev/null
+++ b/testdata/out_matching_transformers.0.documentation
@@ -0,0 +1,16 @@
+Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]}
+Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}]
+Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}}
+Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}}
+Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}}
+Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]}
+Matching: test_reader_as_single_string: matches: matches expectation: "cool"
+Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"]
+Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]}
+Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]}
+Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]}
+Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]}
+
+
+Total Duration:
+Count: 12, Failed: 0, Skipped: 0
diff --git a/testdata/out_matching_transformers.0.nagios b/testdata/out_matching_transformers.0.nagios
new file mode 100644
index 000000000..749fcaed1
--- /dev/null
+++ b/testdata/out_matching_transformers.0.nagios
@@ -0,0 +1 @@
+GOSS OK - Count: 12, Failed: 0, Skipped: 0, Duration:
diff --git a/testdata/out_matching_transformers.0.rspecish b/testdata/out_matching_transformers.0.rspecish
new file mode 100644
index 000000000..a5b03e65c
--- /dev/null
+++ b/testdata/out_matching_transformers.0.rspecish
@@ -0,0 +1,4 @@
+............
+
+Total Duration:
+Count: 12, Failed: 0, Skipped: 0
diff --git a/testdata/out_matching_transformers.0.tap b/testdata/out_matching_transformers.0.tap
new file mode 100644
index 000000000..e01c41d1d
--- /dev/null
+++ b/testdata/out_matching_transformers.0.tap
@@ -0,0 +1,13 @@
+1..12
+ok 1 - Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]}
+ok 2 - Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}]
+ok 3 - Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}}
+ok 4 - Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}}
+ok 5 - Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}}
+ok 6 - Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]}
+ok 7 - Matching: test_reader_as_single_string: matches: matches expectation: "cool"
+ok 8 - Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"]
+ok 9 - Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]}
+ok 10 - Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]}
+ok 11 - Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]}
+ok 12 - Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]}
diff --git a/testdata/out_matching_transformers_failing.1.documentation b/testdata/out_matching_transformers_failing.1.documentation
new file mode 100644
index 000000000..c80967b3d
--- /dev/null
+++ b/testdata/out_matching_transformers_failing.1.documentation
@@ -0,0 +1,320 @@
+Matching: basic_reader_as_array: matches:
+Expected
+ ["foo bar","moo cow",""]
+to contain element matching
+ {"contain-substring":"fox"}
+the transform chain was
+ [{"to-array":{}}]
+the raw value was
+ "foo bar\nmoo cow\n"
+Matching: test_array: matches:
+Expected
+ ["45","46","47"]
+to contain elements
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+the missing elements were
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+Matching: test_gjson_have_key_array: matches:
+Expected
+ {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
+to satisfy at least one of these matchers
+ [{"have-key":"fail"}]
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+Matching: test_gjson_invalid: matches:
+Error
+ matchers.Gjson{Path:"@this"}: Invalid json
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\""
+Matching: test_gjson_not_found: matches:
+Error
+ matchers.Gjson{Path:"foo"}: Path not found: foo
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+Matching: test_gjson_transform_nested_and: matches:
+Expected
+ {"nested":"cow"}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_transform_nested_count: matches:
+Expected
+ 15
+to be numerically le
+ 10
+the transform chain was
+ [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_transform_nested_prefix: matches:
+Expected
+ "bar"
+to have prefix
+ "x"
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_transform_nested_this: matches:
+Expected
+ {"count":"15","foo":"bar","moo":{"nested":"cow"}}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_transform_not_key: matches:
+Expected
+ {"nested":"cow"}
+not to have key matching
+ "nested"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_transform_simple: matches:
+Expected
+ "cow"
+to equal
+ "cowx"
+the transform chain was
+ [{"gjson":{"Path":"moo.nested"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+Matching: test_gjson_using_this_and_equal: matches:
+Expected
+ {"baz":"bing","foo":"bar"}
+to equal
+ {"baz":"bing","fox":"bar"}
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"baz\": \"bing\"}"
+Matching: test_numeric_string: matches:
+Expected
+ "128"
+to equal
+ "129"
+the transform chain was
+ [{"to-string":{}}]
+the raw value was
+ 128
+Matching: test_reader_as_single_string: matches:
+Expected
+ "cool"
+to equal
+ "not-cool"
+Matching: test_reader_using_int_matchers: matches:
+Expected
+ 40
+to be numerically le
+ 20
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "40"
+Matching: test_reader_using_string_matchers: matches:
+Expected
+ "foo bar\n15\nmoo cow\n"
+to have length
+ 15
+Matching: test_string_float: matches:
+Expected
+ 128.3
+to be numerically eq
+ 129.3
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128.3"
+Matching: test_string_numeric: matches:
+Expected
+ 128
+to be numerically eq
+ 129
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128"
+
+
+Failures/Skipped:
+
+Matching: basic_reader_as_array: matches:
+Expected
+ ["foo bar","moo cow",""]
+to contain element matching
+ {"contain-substring":"fox"}
+the transform chain was
+ [{"to-array":{}}]
+the raw value was
+ "foo bar\nmoo cow\n"
+
+Matching: test_array: matches:
+Expected
+ ["45","46","47"]
+to contain elements
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+the missing elements were
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+
+Matching: test_gjson_have_key_array: matches:
+Expected
+ {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
+to satisfy at least one of these matchers
+ [{"have-key":"fail"}]
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+
+Matching: test_gjson_invalid: matches:
+Error
+ matchers.Gjson{Path:"@this"}: Invalid json
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\""
+
+Matching: test_gjson_not_found: matches:
+Error
+ matchers.Gjson{Path:"foo"}: Path not found: foo
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+
+Matching: test_gjson_transform_nested_and: matches:
+Expected
+ {"nested":"cow"}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_count: matches:
+Expected
+ 15
+to be numerically le
+ 10
+the transform chain was
+ [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_prefix: matches:
+Expected
+ "bar"
+to have prefix
+ "x"
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_this: matches:
+Expected
+ {"count":"15","foo":"bar","moo":{"nested":"cow"}}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_not_key: matches:
+Expected
+ {"nested":"cow"}
+not to have key matching
+ "nested"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_simple: matches:
+Expected
+ "cow"
+to equal
+ "cowx"
+the transform chain was
+ [{"gjson":{"Path":"moo.nested"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_using_this_and_equal: matches:
+Expected
+ {"baz":"bing","foo":"bar"}
+to equal
+ {"baz":"bing","fox":"bar"}
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"baz\": \"bing\"}"
+
+Matching: test_numeric_string: matches:
+Expected
+ "128"
+to equal
+ "129"
+the transform chain was
+ [{"to-string":{}}]
+the raw value was
+ 128
+
+Matching: test_reader_as_single_string: matches:
+Expected
+ "cool"
+to equal
+ "not-cool"
+
+Matching: test_reader_using_int_matchers: matches:
+Expected
+ 40
+to be numerically le
+ 20
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "40"
+
+Matching: test_reader_using_string_matchers: matches:
+Expected
+ "foo bar\n15\nmoo cow\n"
+to have length
+ 15
+
+Matching: test_string_float: matches:
+Expected
+ 128.3
+to be numerically eq
+ 129.3
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128.3"
+
+Matching: test_string_numeric: matches:
+Expected
+ 128
+to be numerically eq
+ 129
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128"
+
+Total Duration:
+Count: 18, Failed: 18, Skipped: 0
diff --git a/testdata/out_matching_transformers_failing.1.rspecish b/testdata/out_matching_transformers_failing.1.rspecish
new file mode 100644
index 000000000..273492d6b
--- /dev/null
+++ b/testdata/out_matching_transformers_failing.1.rspecish
@@ -0,0 +1,172 @@
+FFFFFFFFFFFFFFFFFF
+
+Failures/Skipped:
+
+Matching: basic_reader_as_array: matches:
+Expected
+ ["foo bar","moo cow",""]
+to contain element matching
+ {"contain-substring":"fox"}
+the transform chain was
+ [{"to-array":{}}]
+the raw value was
+ "foo bar\nmoo cow\n"
+
+Matching: test_array: matches:
+Expected
+ ["45","46","47"]
+to contain elements
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+the missing elements were
+ [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+
+Matching: test_gjson_have_key_array: matches:
+Expected
+ {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
+to satisfy at least one of these matchers
+ [{"have-key":"fail"}]
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+
+Matching: test_gjson_invalid: matches:
+Error
+ matchers.Gjson{Path:"@this"}: Invalid json
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"arr\""
+
+Matching: test_gjson_not_found: matches:
+Error
+ matchers.Gjson{Path:"foo"}: Path not found: foo
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+
+Matching: test_gjson_transform_nested_and: matches:
+Expected
+ {"nested":"cow"}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_count: matches:
+Expected
+ 15
+to be numerically le
+ 10
+the transform chain was
+ [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_prefix: matches:
+Expected
+ "bar"
+to have prefix
+ "x"
+the transform chain was
+ [{"gjson":{"Path":"foo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_nested_this: matches:
+Expected
+ {"count":"15","foo":"bar","moo":{"nested":"cow"}}
+to have key matching
+ "nope"
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_not_key: matches:
+Expected
+ {"nested":"cow"}
+not to have key matching
+ "nested"
+the transform chain was
+ [{"gjson":{"Path":"moo"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_transform_simple: matches:
+Expected
+ "cow"
+to equal
+ "cowx"
+the transform chain was
+ [{"gjson":{"Path":"moo.nested"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+
+Matching: test_gjson_using_this_and_equal: matches:
+Expected
+ {"baz":"bing","foo":"bar"}
+to equal
+ {"baz":"bing","fox":"bar"}
+the transform chain was
+ [{"gjson":{"Path":"@this"}}]
+the raw value was
+ "{\"foo\": \"bar\", \"baz\": \"bing\"}"
+
+Matching: test_numeric_string: matches:
+Expected
+ "128"
+to equal
+ "129"
+the transform chain was
+ [{"to-string":{}}]
+the raw value was
+ 128
+
+Matching: test_reader_as_single_string: matches:
+Expected
+ "cool"
+to equal
+ "not-cool"
+
+Matching: test_reader_using_int_matchers: matches:
+Expected
+ 40
+to be numerically le
+ 20
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "40"
+
+Matching: test_reader_using_string_matchers: matches:
+Expected
+ "foo bar\n15\nmoo cow\n"
+to have length
+ 15
+
+Matching: test_string_float: matches:
+Expected
+ 128.3
+to be numerically eq
+ 129.3
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128.3"
+
+Matching: test_string_numeric: matches:
+Expected
+ 128
+to be numerically eq
+ 129
+the transform chain was
+ [{"to-numeric":{}}]
+the raw value was
+ "128"
+
+Total Duration:
+Count: 18, Failed: 18, Skipped: 0
diff --git a/testdata/out_matching_transformers_failing.1.tap b/testdata/out_matching_transformers_failing.1.tap
new file mode 100644
index 000000000..851a8c6ac
--- /dev/null
+++ b/testdata/out_matching_transformers_failing.1.tap
@@ -0,0 +1,19 @@
+1..18
+not ok 1 - Matching: basic_reader_as_array: matches: Expected ["foo bar","moo cow",""] to contain element matching {"contain-substring":"fox"} the transform chain was [{"to-array":{}}] the raw value was "foo bar\nmoo cow\n"
+not ok 2 - Matching: test_array: matches: Expected ["45","46","47"] to contain elements [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] the missing elements were [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
+not ok 3 - Matching: test_gjson_have_key_array: matches: Expected {"arr":[{"nested":"cow"},{"nested2":"moo"}]} to satisfy at least one of these matchers [{"have-key":"fail"}] the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+not ok 4 - Matching: test_gjson_invalid: matches: Error matchers.Gjson{Path:"@this"}: Invalid json the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\""
+not ok 5 - Matching: test_gjson_not_found: matches: Error matchers.Gjson{Path:"foo"}: Path not found: foo the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
+not ok 6 - Matching: test_gjson_transform_nested_and: matches: Expected {"nested":"cow"} to have key matching "nope" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 7 - Matching: test_gjson_transform_nested_count: matches: Expected 15 to be numerically le 10 the transform chain was [{"gjson":{"Path":"count"}},{"to-numeric":{}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 8 - Matching: test_gjson_transform_nested_prefix: matches: Expected "bar" to have prefix "x" the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 9 - Matching: test_gjson_transform_nested_this: matches: Expected {"count":"15","foo":"bar","moo":{"nested":"cow"}} to have key matching "nope" the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 10 - Matching: test_gjson_transform_not_key: matches: Expected {"nested":"cow"} not to have key matching "nested" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 11 - Matching: test_gjson_transform_simple: matches: Expected "cow" to equal "cowx" the transform chain was [{"gjson":{"Path":"moo.nested"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
+not ok 12 - Matching: test_gjson_using_this_and_equal: matches: Expected {"baz":"bing","foo":"bar"} to equal {"baz":"bing","fox":"bar"} the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"baz\": \"bing\"}"
+not ok 13 - Matching: test_numeric_string: matches: Expected "128" to equal "129" the transform chain was [{"to-string":{}}] the raw value was 128
+not ok 14 - Matching: test_reader_as_single_string: matches: Expected "cool" to equal "not-cool"
+not ok 15 - Matching: test_reader_using_int_matchers: matches: Expected 40 to be numerically le 20 the transform chain was [{"to-numeric":{}}] the raw value was "40"
+not ok 16 - Matching: test_reader_using_string_matchers: matches: Expected "foo bar\n15\nmoo cow\n" to have length 15
+not ok 17 - Matching: test_string_float: matches: Expected 128.3 to be numerically eq 129.3 the transform chain was [{"to-numeric":{}}] the raw value was "128.3"
+not ok 18 - Matching: test_string_numeric: matches: Expected 128 to be numerically eq 129 the transform chain was [{"to-numeric":{}}] the raw value was "128"
diff --git a/testdata/out_matching_transformers_failing.2.nagios b/testdata/out_matching_transformers_failing.2.nagios
new file mode 100644
index 000000000..3e7a6bbf7
--- /dev/null
+++ b/testdata/out_matching_transformers_failing.2.nagios
@@ -0,0 +1 @@
+GOSS CRITICAL - Count: 18, Failed: 18, Skipped: 0, Duration:
diff --git a/util/config.go b/util/config.go
index 9185bd479..81751aed7 100644
--- a/util/config.go
+++ b/util/config.go
@@ -43,6 +43,7 @@ type Config struct {
Proxy string
RequestHeader []string
RetryTimeout time.Duration
+ RunLevel string
Server string
Sleep time.Duration
Spec string
diff --git a/validate.go b/validate.go
index a967d9387..9e4c394cc 100644
--- a/validate.go
+++ b/validate.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/fatih/color"
+ "github.com/onsi/gomega/format"
"github.com/goss-org/goss/outputs"
"github.com/goss-org/goss/resource"
@@ -98,18 +99,26 @@ func ValidateResults(c *util.Config) (results <-chan []resource.TestResult, err
// and supports retries and more, this is the full featured Validate used
// by the typical CLI invocation and will produce output to StdOut. Use
// ValidateResults for programmatic access
-func Validate(c *util.Config, startTime time.Time) (code int, err error) {
+func Validate(c *util.Config) (code int, err error) {
err = setLogLevel(c)
if err != nil {
return 1, err
}
- outputConfig := util.OutputConfig{
- FormatOptions: c.FormatOptions,
- }
-
gossConfig, err := getGossConfig(c.Vars, c.VarsInline, c.Spec)
if err != nil {
- return 1, err
+ return 78, err
+ }
+ return ValidateConfig(c, gossConfig)
+}
+
+func ValidateConfig(c *util.Config, gossConfig *GossConfig) (code int, err error) {
+ // Needed for contains-elements
+ // Maybe we don't use this and use custom
+ // contain_element_matcher is needed because it's single entry to avoid
+ // transform message
+ format.UseStringerRepresentation = true
+ outputConfig := util.OutputConfig{
+ FormatOptions: c.FormatOptions,
}
sys := system.New(c.PackageManager)
@@ -127,10 +136,10 @@ func Validate(c *util.Config, startTime time.Time) (code int, err error) {
sleep := c.Sleep
retryTimeout := c.RetryTimeout
i := 1
+ startTime := time.Now()
for {
- iStartTime := time.Now()
out := validate(sys, *gossConfig, c.DisabledResourceTypes, c.MaxConcurrent)
- exitCode := outputer.Output(ofh, out, iStartTime, outputConfig)
+ exitCode := outputer.Output(ofh, out, outputConfig)
if retryTimeout == 0 || exitCode == 0 {
return exitCode, nil
}