complement/must/must.go
kegsay 5ecb086412
refactor: make logging test agnostic and colourful (#703)
- The entire Complement API now no longer uses `t *testing.T`, instead
  opting for `ct.TestLike` which is an interface. In practice, this changes
  nothing. However, this enables Complement to be used in many more places
  where you do not have a `t *testing.T` object to use e.g benchmarks, scripts.
  This is of particular use for Complement-Crypto which has to run parts of the
  test as a standalone binary, and therefore has no `t *testing.T` to use.
- The entire Complement API now uses `ct.Fatalf` and `ct.Errorf` for highlighting
  test failures in red. This should make it significantly easier to skim for the
  test failure message.
- Add `Deployment.ContainerID(TestLike, hsName) string` to allow tests to interact
  with the container beyond the abilities of the Complement API e.g log extraction,
  memory use, CPU use.
2024-01-17 12:41:04 +00:00

231 lines
6.9 KiB
Go

// Package must contains assertions for tests, which fail the test if the assertion fails.
package must
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"github.com/tidwall/gjson"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/complement/ct"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/should"
)
const ansiRedForeground = "\x1b[31m"
const ansiResetForeground = "\x1b[39m"
// NotError will ensure `err` is nil else terminate the test with `msg`.
func NotError(t ct.TestLike, msg string, err error) {
t.Helper()
if err != nil {
ct.Fatalf(t, "must.NotError: %s -> %s", msg, err)
}
}
// EXPERIMENTAL
// ParseJSON will ensure that the HTTP request/response body is valid JSON, then return the body, else terminate the test.
func ParseJSON(t ct.TestLike, b io.ReadCloser) gjson.Result {
t.Helper()
res, err := should.ParseJSON(b)
if err != nil {
ct.Fatalf(t, err.Error())
}
return res
}
// EXPERIMENTAL
// MatchRequest consumes the HTTP request and performs HTTP-level assertions on it. Returns the raw response body.
func MatchRequest(t ct.TestLike, req *http.Request, m match.HTTPRequest) []byte {
t.Helper()
res, err := should.MatchRequest(req, m)
if err != nil {
ct.Fatalf(t, err.Error())
}
return res
}
// EXPERIMENTAL
// MatchSuccess consumes the HTTP response and fails if the response is non-2xx.
func MatchSuccess(t ct.TestLike, res *http.Response) {
t.Helper()
if err := should.MatchSuccess(res); err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// MatchFailure consumes the HTTP response and fails if the response is 2xx.
func MatchFailure(t ct.TestLike, res *http.Response) {
t.Helper()
if err := should.MatchFailure(res); err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// MatchResponse consumes the HTTP response and performs HTTP-level assertions on it. Returns the raw response body.
func MatchResponse(t ct.TestLike, res *http.Response, m match.HTTPResponse) []byte {
t.Helper()
body, err := should.MatchResponse(res, m)
if err != nil {
ct.Fatalf(t, err.Error())
}
return body
}
// MatchFederationRequest performs JSON assertions on incoming federation requests.
func MatchFederationRequest(t ct.TestLike, fedReq *fclient.FederationRequest, matchers ...match.JSON) {
t.Helper()
err := should.MatchFederationRequest(fedReq)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// MatchGJSON performs JSON assertions on a gjson.Result object.
func MatchGJSON(t ct.TestLike, jsonResult gjson.Result, matchers ...match.JSON) {
t.Helper()
err := should.MatchGJSON(jsonResult, matchers...)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// MatchJSONBytes performs JSON assertions on a raw json byte slice.
func MatchJSONBytes(t ct.TestLike, rawJson []byte, matchers ...match.JSON) {
t.Helper()
err := should.MatchJSONBytes(rawJson, matchers...)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// Equal ensures that got==want else logs an error.
// The 'msg' is displayed with the error to provide extra context.
func Equal[V comparable](t ct.TestLike, got, want V, msg string) {
t.Helper()
if got != want {
ct.Errorf(t, "Equal %s: got '%v' want '%v'", msg, got, want)
}
}
// NotEqual ensures that got!=want else logs an error.
// The 'msg' is displayed with the error to provide extra context.
func NotEqual[V comparable](t ct.TestLike, got, want V, msg string) {
t.Helper()
if got == want {
ct.Errorf(t, "NotEqual %s: got '%v', want '%v'", msg, got, want)
}
}
// EXPERIMENTAL
// StartWithStr ensures that got starts with wantPrefix else logs an error.
func StartWithStr(t ct.TestLike, got, wantPrefix, msg string) {
t.Helper()
if !strings.HasPrefix(got, wantPrefix) {
ct.Errorf(t, "StartWithStr: %s: got '%s' without prefix '%s'", msg, got, wantPrefix)
}
}
// GetJSONFieldStr extracts the string value under `wantKey` or fails the test.
// The format of `wantKey` is specified at https://godoc.org/github.com/tidwall/gjson#Get
func GetJSONFieldStr(t ct.TestLike, body gjson.Result, wantKey string) string {
t.Helper()
str, err := should.GetJSONFieldStr(body, wantKey)
if err != nil {
ct.Fatalf(t, err.Error())
}
return str
}
// EXPERIMENTAL
// HaveInOrder checks that the two slices match exactly, failing the test on mismatches or omissions.
func HaveInOrder[V comparable](t ct.TestLike, gots []V, wants []V) {
t.Helper()
err := should.HaveInOrder(gots, wants)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// ContainSubset checks that every item in smaller is in larger, failing the test if at least 1 item isn't. Ignores extra elements
// in larger. Ignores ordering.
func ContainSubset[V comparable](t ct.TestLike, larger []V, smaller []V) {
t.Helper()
err := should.ContainSubset(larger, smaller)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// NotContainSubset checks that every item in smaller is NOT in larger, failing the test if at least 1 item is. Ignores extra elements
// in larger. Ignores ordering.
func NotContainSubset[V comparable](t ct.TestLike, larger []V, smaller []V) {
t.Helper()
err := should.NotContainSubset(larger, smaller)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// CheckOffAll checks that a list contains exactly the given items, in any order.
//
// if an item is not present, the test is failed.
// if an item not present in the want list is present, the test is failed.
// Items are compared using match.JSONDeepEqual
func CheckOffAll(t ct.TestLike, items []interface{}, wantItems []interface{}) {
t.Helper()
err := should.CheckOffAll(items, wantItems)
if err != nil {
ct.Fatalf(t, err.Error())
}
}
// EXPERIMENTAL
// CheckOffAllAllowUnwanted checks that a list contains all of the given items, in any order.
// The updated list with the matched items removed from it is returned.
//
// if an item is not present, the test is failed.
// Items are compared using match.JSONDeepEqual
func CheckOffAllAllowUnwanted(t ct.TestLike, items []interface{}, wantItems []interface{}) []interface{} {
t.Helper()
result, err := should.CheckOffAllAllowUnwanted(items, wantItems)
if err != nil {
ct.Fatalf(t, err.Error())
}
return result
}
// EXPERIMENTAL
// CheckOff an item from the list. If the item is not present the test is failed.
// The updated list with the matched item removed from it is returned. Items are
// compared using JSON deep equal.
func CheckOff(t ct.TestLike, items []interface{}, wantItem interface{}) []interface{} {
t.Helper()
result, err := should.CheckOff(items, wantItem)
if err != nil {
ct.Fatalf(t, err.Error())
}
return result
}
func jsonDeepEqual(gotJson []byte, wantValue interface{}) bool {
// marshal what the test gave us
wantBytes, _ := json.Marshal(wantValue)
// re-marshal what the network gave us to acount for key ordering
var gotVal interface{}
_ = json.Unmarshal(gotJson, &gotVal)
gotBytes, _ := json.Marshal(gotVal)
return bytes.Equal(gotBytes, wantBytes)
}