pax_global_header00006660000000000000000000000064143034412740014514gustar00rootroot0000000000000052 comment=58e4887ffe16320208d6cf0e0ffb7085175fdcaf go-test-helpers-3.0.2/000077500000000000000000000000001430344127400145405ustar00rootroot00000000000000go-test-helpers-3.0.2/.circleci/000077500000000000000000000000001430344127400163735ustar00rootroot00000000000000go-test-helpers-3.0.2/.circleci/config.yml000066400000000000000000000040261430344127400203650ustar00rootroot00000000000000version: 2.1 orbs: win: circleci/windows@1.0.0 workflows: workflow: jobs: - go-test: name: Go 1.19 docker-image: cimg/go:1.19 run-lint: true - go-test: name: Go 1.18 docker-image: cimg/go:1.18 - go-test-windows: name: Windows jobs: go-test: parameters: docker-image: type: string run-lint: type: boolean default: false docker: - image: <> environment: CIRCLE_TEST_REPORTS: /tmp/circle-reports CIRCLE_ARTIFACTS: /tmp/circle-artifacts steps: - checkout - run: name: install go-junit-report command: go install github.com/jstemmer/go-junit-report/v2@v2.0.0 - when: condition: <> steps: - run: make lint - run: name: Run tests command: | mkdir -p $CIRCLE_TEST_REPORTS mkdir -p $CIRCLE_ARTIFACTS trap "go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml" EXIT make test | tee $CIRCLE_ARTIFACTS/report.txt - store_test_results: path: /tmp/circle-reports - store_artifacts: path: /tmp/circle-artifacts go-test-windows: executor: name: win/vs2019 shell: powershell.exe environment: GOPATH: C:\Users\VssAdministrator\go steps: - checkout - run: name: download Go 1.18.5 command: | $ErrorActionPreference = "Stop" $installerUrl = "https://go.dev/dl/go1.18.5.windows-amd64.msi" (New-Object System.Net.WebClient).DownloadFile($installerUrl, "go1.18.5.windows-amd64.msi") - run: name: install Go 1.18.5 command: Start-Process msiexec.exe -Wait -ArgumentList "/I go1.18.5.windows-amd64.msi /quiet" - run: go version - run: name: build and test command: | go test -race ./... go-test-helpers-3.0.2/.gitignore000066400000000000000000000000051430344127400165230ustar00rootroot00000000000000bin/ go-test-helpers-3.0.2/.golangci.yml000066400000000000000000000014161430344127400171260ustar00rootroot00000000000000run: deadline: 120s tests: false linters: enable: - bodyclose - deadcode - depguard - dupl - errcheck - goconst - gochecknoglobals - gochecknoinits - goconst - gocritic - gocyclo - godox - gofmt - goimports - gosec - gosimple - govet - ineffassign - lll - megacheck - misspell - nakedret - nolintlint - prealloc - revive - staticcheck - stylecheck - typecheck - unconvert - unparam - unused - varcheck - whitespace fast: false linters-settings: gofmt: simplify: false goimports: local-prefixes: gopkg.in/launchdarkly,github.com/launchdarkly issues: exclude-use-default: false max-same-issues: 1000 max-per-linter: 1000 go-test-helpers-3.0.2/.ldrelease/000077500000000000000000000000001430344127400165565ustar00rootroot00000000000000go-test-helpers-3.0.2/.ldrelease/config.yml000066400000000000000000000003701430344127400205460ustar00rootroot00000000000000version: 2 jobs: - docker: image: golang:1.18-buster template: name: go branches: - name: main - name: 2.x publications: - url: https://pkg.go.dev/github.com/launchdarkly/go-test-helpers/v3 description: documentation go-test-helpers-3.0.2/CHANGELOG.md000066400000000000000000000062441430344127400163570ustar00rootroot00000000000000# Change log All notable changes to the project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). ## [3.0.2] - 2022-08-30 ### Fixed: - Test assertion helpers now call `t.Helper()` to exclude themselves from stacktraces. ## [3.0.1] - 2022-08-29 ### Fixed: - Fixed import paths to have `/v3`. ## [3.0.0] - 2022-08-29 ### Added: - `TryReceive`, `RequireValue`, `AssertNoMoreValues`, `AssertChannelClosed`, `AssertChannelNotClosed`: new generics-based helpers related to channels. - `WithTempFileData`, `WithTempDir`: new filesystem helpers. - `jsonhelpers.AssertEqual`, `jsonhelpers.Value`: a simpler way to do JSON equality assertions and get the diff format that's provided by this package, without having to use the `matchers` API. - `testbox.ShouldFail`, `testbox.ShouldFailAndExitEarly`: for making assertions about assertion logic. ### Changed: - The minimum Go version is now 1.18. ## [2.3.2] - 2022-05-02 ### Changed: - Improved the panic message that is generated by `httphelpers.BrokenConnectionHandler` to make it clearer that this is an intentional error (if for instance the panic stacktrace is logged by `http.Server`). ## [2.3.1] - 2022-02-03 ### Fixed: - The `Assert`/`Require` methods in `matchers` now call `t.Helper()` (if that method exists) so the location of the failure will be more accurately reported. ## [2.3.0] - 2022-01-21 ### Added: - Subpackages `jsonhelpers` and `matchers`. ## [2.2.0] - 2020-07-06 ### Added: - Added `RetryMillis` field to `httphelpers.SSEEvent`. ## [2.1.0] - 2020-07-06 ### Added: - In `httphelpers.SSEStreamControl`: `EnqueueComment` and `SendComment`. ## [2.0.1] - 2020-07-02 ### Fixed: - Fixed a bug in `ChunkedStreamingHandler` that caused the response to hang before reading the headers if there was no initial data in the stream. ## [2.0.0] - 2020-07-01 ### Added: - In `httphelpers`, `ChunkedStreamingHandler`, `SSEHandler`, and `SSEEvent` - In the main package, `ReadWithTimeout`. ### Changed: - This project now requires modules and has a minimum Go version of 1.13. - In `ldservices`, `ServerSideStreamingServiceHandler` now uses `httphelpers.SSEEvent` instead of having a dependency on the `eventsource` package. Its interface is now based on the new `SSEHandler` instead of using channels. - In `httphelpers`, `PanicHandler` is replaced by `BrokenConnectionHandler`. ### Removed: - The LaunchDarkly client-side streaming endpoint handler in `ldservices` was not used and has been removed. ## [1.2.0] - 2020-07-01 ### Added: - New package `testbox` for running a Go test in a temporary environment. ## [1.1.3] - 2020-06-04 ### Added: - Added `go.mod` so this package can be consumed as a module. This does not affect code that is currently consuming it via `go get`, `dep`, or `govendor`. ## [1.1.2] - 2020-04-01 ### Fixed: - Patch event type for client-side streams. ## [1.1.1] - 2020-04-01 ### Fixed: - In `ldservices`, fixed JSON property names for simulated client-side flag data. ## [1.1.0] - 2020-04-01 ### Added: - Method `HandlerForPathRegex` in `httphelpers`. - New methods and types in `ldservices` to simulate LaunchDarkly client-side streaming endpoints. ## [1.0.0] - 2020-03-16 Initial release. go-test-helpers-3.0.2/CONTRIBUTING.md000066400000000000000000000022271430344127400167740ustar00rootroot00000000000000# Contributing to go-test-helpers ## Submitting bug reports and feature requests The LaunchDarkly SDK team maintains this repository and monitors the [issue tracker](https://github.com/launchdarkly/go-test-helpers/issues) there. Bug reports and feature requests specific to this project should be filed in this issue tracker. The team will respond to all newly filed issues within two business days. ## Submitting pull requests We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. ## Build instructions ### Prerequisites This project should be built against Go 1.13 or newer. ### Building To build the project without running any tests: ``` make ``` If you wish to clean your working directory between builds, you can clean it by running: ``` make clean ``` To run the linter: ``` make lint ``` ### Testing To build the project and run all unit tests: ``` make test ``` go-test-helpers-3.0.2/LICENSE.txt000066400000000000000000000010551430344127400163640ustar00rootroot00000000000000Copyright 2020 Catamorphic, Co. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. go-test-helpers-3.0.2/Makefile000066400000000000000000000007461430344127400162070ustar00rootroot00000000000000 GOLANGCI_LINT_VERSION=v1.48.0 LINTER=./bin/golangci-lint LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION) .PHONY: build clean test lint build: go build ./... clean: go clean test: build go test -race -v ./... $(LINTER_VERSION_FILE): rm -f $(LINTER) curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION) touch $(LINTER_VERSION_FILE) lint: $(LINTER_VERSION_FILE) $(LINTER) run ./... go-test-helpers-3.0.2/README.md000066400000000000000000000076051430344127400160270ustar00rootroot00000000000000# LaunchDarkly Go Test Helpers [![Circle CI](https://circleci.com/gh/launchdarkly/go-test-helpers.svg?style=svg)](https://circleci.com/gh/launchdarkly/go-test-helpers) [![Documentation](https://img.shields.io/static/v1?label=go.dev&message=reference&color=00add8)](https://pkg.go.dev/github.com/launchdarkly/go-test-helpers) This project centralizes some test support code that is used by LaunchDarkly's Go SDK and related components, and that may be useful in other Go projects. While this code may be useful in other projects, it is primarily geared toward LaunchDarkly's own development needs and is not meant to provide a large general-purpose framework. It is meant for unit test code and should not be used as a runtime dependency. This version of the project requires Go 1.18 or higher. ## Contents The main package provides general-purpose helper functions. Subpackage `httphelpers` provides convenience wrappers for using `net/http` and `net/http/httptest` in test code. Subpackage `jsonhelpers` provides functions for manipulating JSON. Subpackage `matchers` contains a test assertion API with combinators. Subpackage `testbox` provides the ability to write tests-of-tests within the Go testing framework. ## Usage Import any of these packages in your test code: ```go import ( "github.com/launchdarkly/go-test-helpers/v3" "github.com/launchdarkly/go-test-helpers/v3/httphelpers" "github.com/launchdarkly/go-test-helpers/v3/jsonhelpers" "github.com/launchdarkly/go-test-helpers/v3/ldservices" "github.com/launchdarkly/go-test-helpers/v3/testbox" ) ``` Breaking changes will only be made in a new major version. It is advisable to use a dependency manager to pin these dependencies to a module version or a major version branch. ## Contributing We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this project. ## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies go-test-helpers-3.0.2/channels.go000066400000000000000000000067021430344127400166670ustar00rootroot00000000000000package helpers import ( "fmt" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TryReceive waits for a value from the channel and returns (value, true, false) if // successful; (, false, false) if the timeout expired first; or // (, false, true) if the channel was closed. func TryReceive[V any](ch <-chan V, timeout time.Duration) (V, bool, bool) { deadline := time.NewTimer(timeout) defer deadline.Stop() select { case v, ok := <-ch: if ok { return v, true, false } return v, false, true case <-deadline.C: var empty V return empty, false, false } } // RequireValue returns the next value from the channel, or forces an immediate test failure // and exit if the timeout expires first. func RequireValue[V any](t require.TestingT, ch <-chan V, timeout time.Duration, customMessageAndArgs ...any) V { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } v, ok, closed := TryReceive(ch, timeout) if ok { return v } var empty V if closed { failWithMessageAndArgs(t, customMessageAndArgs, "expected a %T value from channel but the channel was closed", empty) } else { failWithMessageAndArgs(t, customMessageAndArgs, "expected a %T value from channel but did not receive one in %s", empty, timeout) } t.FailNow() return empty // never reached } // AssertNoMoreValues asserts that no value is available from the channel within the timeout, // but that the channel was not closed. func AssertNoMoreValues[V any]( t assert.TestingT, ch <-chan V, timeout time.Duration, customMessageAndArgs ...any, ) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } v, ok, closed := TryReceive(ch, timeout) if ok { failWithMessageAndArgs(t, customMessageAndArgs, "expected no more %T values from channel but got one: %+v", v, v) return false } if closed { failWithMessageAndArgs(t, customMessageAndArgs, "channel was unexpectedly closed") return false } return true } // AssertChannelClosed asserts that the channel is closed within the timeout, sending no values. func AssertChannelClosed[V any]( t assert.TestingT, ch <-chan V, timeout time.Duration, customMessageAndArgs ...any, ) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } v, ok, closed := TryReceive(ch, timeout) if ok { failWithMessageAndArgs(t, customMessageAndArgs, "expected no more %T values from channel but got one: %+v", v, v) return false } if !closed { failWithMessageAndArgs(t, customMessageAndArgs, "expected channel to be closed within %s but it was not", timeout) return false } return true } // AssertChannelNotClosed asserts that the channel is not closed within the timeout, consuming // any values that may be sent during that time. func AssertChannelNotClosed[V any]( t assert.TestingT, ch <-chan V, timeout time.Duration, customMessageAndArgs ...any, ) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } deadline := time.NewTimer(timeout) defer deadline.Stop() for { select { case _, ok := <-ch: if !ok { failWithMessageAndArgs(t, customMessageAndArgs, "channel was unexpectedly closed") return false } case <-deadline.C: return true } } } func failWithMessageAndArgs(t assert.TestingT, customMessageAndArgs []any, defaultMsg string, defaultArgs ...any) { t.Errorf(defaultMsg, defaultArgs...) if len(customMessageAndArgs) != 0 { t.Errorf(fmt.Sprintf("%s", customMessageAndArgs[0]), customMessageAndArgs[1:]...) } } go-test-helpers-3.0.2/channels_test.go000066400000000000000000000101101430344127400177120ustar00rootroot00000000000000package helpers import ( "testing" "time" "github.com/launchdarkly/go-test-helpers/v3/testbox" "github.com/stretchr/testify/assert" ) func TestTryReceive(t *testing.T) { ch := make(chan string, 1) v, ok, closed := TryReceive(ch, time.Millisecond) assert.False(t, ok) assert.False(t, closed) assert.Equal(t, "", v) ch <- "a" v, ok, closed = TryReceive(ch, time.Millisecond) assert.True(t, ok) assert.False(t, closed) assert.Equal(t, "a", v) go func() { close(ch) }() v, ok, closed = TryReceive(ch, time.Second) assert.False(t, ok) assert.True(t, closed) assert.Equal(t, "", v) } func TestRequireValue(t *testing.T) { testbox.ShouldFailAndExitEarly(t, func(t testbox.TestingT) { ch := make(chan string, 1) _ = RequireValue(t, ch, time.Millisecond) }) ch := make(chan string, 1) go func() { ch <- "a" }() v := RequireValue(t, ch, time.Second) assert.Equal(t, "a", v) testbox.ShouldFailAndExitEarly(t, func(t testbox.TestingT) { ch := make(chan string, 1) go func() { close(ch) }() _ = RequireValue(t, ch, time.Second) }) } func TestAssertNoMoreValues(t *testing.T) { ch := make(chan string, 1) AssertNoMoreValues(t, ch, time.Millisecond) testbox.ShouldFail(t, func(t testbox.TestingT) { ch := make(chan string, 1) go func() { ch <- "a" }() AssertNoMoreValues(t, ch, time.Second) }) testbox.ShouldFail(t, func(t testbox.TestingT) { ch := make(chan string, 1) go func() { close(ch) }() AssertNoMoreValues(t, ch, time.Second) }) } func TestAssertChannelClosed(t *testing.T) { ch := make(chan string, 1) go func() { close(ch) }() AssertChannelClosed(t, ch, time.Second) testbox.ShouldFail(t, func(t testbox.TestingT) { ch := make(chan string, 1) AssertChannelClosed(t, ch, time.Millisecond) }) testbox.ShouldFail(t, func(t testbox.TestingT) { ch := make(chan string, 1) ch <- "a" AssertChannelClosed(t, ch, time.Millisecond) }) } func TestAssertChannelNotClosed(t *testing.T) { testbox.ShouldFail(t, func(t testbox.TestingT) { ch := make(chan string, 1) go func() { close(ch) }() AssertChannelNotClosed(t, ch, time.Second) }) ch := make(chan string, 1) AssertChannelNotClosed(t, ch, time.Millisecond) ch <- "a" AssertChannelNotClosed(t, ch, time.Millisecond) } func TestFailureMessages(t *testing.T) { result := testbox.SandboxTest(func(t testbox.TestingT) { ch := make(chan string, 1) _ = RequireValue(t, ch, time.Millisecond, "sorry%s", ".") }) if assert.Len(t, result.Failures, 2) { assert.Equal(t, "expected a string value from channel but did not receive one in 1ms", result.Failures[0].Message) assert.Equal(t, "sorry.", result.Failures[1].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { ch := make(chan string, 1) go func() { ch <- "a" }() AssertNoMoreValues(t, ch, time.Second, "sorry%s", ".") }) if assert.Len(t, result.Failures, 2) { assert.Equal(t, "expected no more string values from channel but got one: a", result.Failures[0].Message) assert.Equal(t, "sorry.", result.Failures[1].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { ch := make(chan string, 1) go func() { close(ch) }() AssertNoMoreValues(t, ch, time.Second, "sorry%s", ".") }) if assert.Len(t, result.Failures, 2) { assert.Equal(t, "channel was unexpectedly closed", result.Failures[0].Message) assert.Equal(t, "sorry.", result.Failures[1].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { ch := make(chan string, 1) AssertChannelClosed(t, ch, time.Millisecond, "sorry%s", ".") }) if assert.Len(t, result.Failures, 2) { assert.Equal(t, "expected channel to be closed within 1ms but it was not", result.Failures[0].Message) assert.Equal(t, "sorry.", result.Failures[1].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { ch := make(chan string, 1) go func() { close(ch) }() AssertChannelNotClosed(t, ch, time.Second, "sorry%s", ".") }) if assert.Len(t, result.Failures, 2) { assert.Equal(t, "channel was unexpectedly closed", result.Failures[0].Message) assert.Equal(t, "sorry.", result.Failures[1].Message) } } go-test-helpers-3.0.2/closer.go000066400000000000000000000012371430344127400163610ustar00rootroot00000000000000package helpers import ( "io" "log" ) // WithCloser executes a function and ensures that the given object's Close() method is always called afterward. // // This is simply a way to get more specific control over an object's lifetime than using defer. A test function // may wish to ensure that an object is closed before some subsequent actions are taken, rather than at the end // of the entire test. // // If closing the object fails, an error is logged. func WithCloser(closeableObject io.Closer, action func()) { defer func() { err := closeableObject.Close() if err != nil { log.Printf("failed to close %T: %s", closeableObject, err) } }() action() } go-test-helpers-3.0.2/closer_test.go000066400000000000000000000005031430344127400174130ustar00rootroot00000000000000package helpers import ( "testing" "github.com/stretchr/testify/assert" ) type myCloser struct { closed bool } func (m *myCloser) Close() error { m.closed = true return nil } func TestWithCloser(t *testing.T) { c := &myCloser{} WithCloser(c, func() { assert.False(t, c.closed) }) assert.True(t, c.closed) } go-test-helpers-3.0.2/files.go000066400000000000000000000033411430344127400161720ustar00rootroot00000000000000package helpers import ( "fmt" "log" "os" ) // FilePathExists is simply a shortcut for using os.Stat to check for a file's or directory's existence. func FilePathExists(path string) bool { _, err := os.Stat(path) return !os.IsNotExist(err) } // WithTempFile creates a temporary file, passes its name to the given function, then ensures that the file is deleted. // // If for any reason it is not possible to create the file, a panic is raised since the test code cannot continue. // // If deletion of the file fails (assuming it has not already been deleted) then an error is logged, but there is no // panic. // // helpers.WithTempFile(func(path string) { // DoSomethingWithTempFile(path) // }) // the file is deleted at the end of this block func WithTempFile(f func(filePath string)) { file, err := os.CreateTemp("", "test") if err != nil { panic(fmt.Errorf("can't create temp file: %s", err)) } _ = file.Close() path := file.Name() defer (func() { if FilePathExists(path) { err := os.Remove(path) if err != nil { log.Printf("Could not delete temp file %s: %s", path, err) } } })() f(file.Name()) } // WithTempFileData is identical to WithTempFile except that it prepopulates the file with the // specified data. func WithTempFileData(data []byte, f func(filePath string)) { WithTempFile(func(filePath string) { if err := os.WriteFile(filePath, data, 0600); err != nil { panic(fmt.Errorf("can't write to temp file: %s", err)) } f(filePath) }) } // WithTempDir creates a temporary directory, calls the function with its path, then removes it. func WithTempDir(f func(path string)) { path, err := os.MkdirTemp("", "test") if err != nil { panic(err) } defer os.RemoveAll(path) //nolint:errcheck f(path) } go-test-helpers-3.0.2/files_test.go000066400000000000000000000016671430344127400172420ustar00rootroot00000000000000package helpers import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWithTempFile(t *testing.T) { var filePath string WithTempFile(func(path string) { filePath = path assert.True(t, FilePathExists(path)) }) assert.False(t, FilePathExists(filePath)) } func TestWithTempFileData(t *testing.T) { var filePath string WithTempFileData([]byte(`hello`), func(path string) { filePath = path data, err := os.ReadFile(path) require.NoError(t, err) assert.Equal(t, "hello", string(data)) }) assert.False(t, FilePathExists(filePath)) } func TestWithTempDir(t *testing.T) { var path string WithTempDir(func(dirPath string) { path = dirPath info, err := os.Stat(path) require.NoError(t, err) assert.True(t, info.IsDir()) assert.NoError(t, os.WriteFile(filepath.Join(dirPath, "x"), []byte("hello"), 0600)) }) assert.False(t, FilePathExists(path)) } go-test-helpers-3.0.2/go.mod000066400000000000000000000003721430344127400156500ustar00rootroot00000000000000module github.com/launchdarkly/go-test-helpers/v3 go 1.18 require github.com/stretchr/testify v1.5.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect ) go-test-helpers-3.0.2/go.sum000066400000000000000000000021601430344127400156720ustar00rootroot00000000000000github.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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= go-test-helpers-3.0.2/httphelpers/000077500000000000000000000000001430344127400171025ustar00rootroot00000000000000go-test-helpers-3.0.2/httphelpers/certificates.go000066400000000000000000000120131430344127400220730ustar00rootroot00000000000000package httphelpers import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "log" "math/big" "net" "net/http" "net/http/httptest" "os" "time" ) // WithSelfSignedServer is a convenience function for starting a test HTTPS server with a self-signed // certificate, running the specified function, and then closing the server and cleaning up the // temporary certificate files. If for some reason creating the server fails, it panics. The action // function's second and third parameters provide the CA certificate for configuring the client, // and a preconfigured CertPool in case that is more convenient to use. func WithSelfSignedServer(handler http.Handler, action func(*httptest.Server, []byte, *x509.CertPool)) { certFile, err := os.CreateTemp("", "test") if err != nil { panic(fmt.Errorf("can't create temp file: %s", err)) } _ = certFile.Close() certFilePath := certFile.Name() tryToDelete := func(path string) { err := os.Remove(path) if err != nil { log.Printf("Unable to clean up temp file %s: %s", path, err) } } defer tryToDelete(certFilePath) keyFile, err := os.CreateTemp("", "test") if err != nil { panic(fmt.Errorf("can't create temp file: %s", err)) } _ = keyFile.Close() keyFilePath := keyFile.Name() defer tryToDelete(keyFilePath) err = MakeSelfSignedCert(certFilePath, keyFilePath) if err != nil { panic(fmt.Errorf("can't create self-signed certificate: %s", err)) } certData, err := os.ReadFile(certFilePath) //nolint:gosec if err != nil { panic(fmt.Errorf("can't read self-signed certificate: %s", err)) } certPool, err := x509.SystemCertPool() if err != nil { certPool = x509.NewCertPool() // necessary in order to work on Windows } certPool.AppendCertsFromPEM(certData) server, err := MakeServerWithCert(certFilePath, keyFilePath, handler) if err != nil { panic(fmt.Errorf("can't start HTTPS server: %s", err)) } defer server.Close() defer server.CloseClientConnections() action(server, certData, certPool) } // MakeServerWithCert creates and starts a test HTTPS server using the specified certificate. func MakeServerWithCert(certFilePath, keyFilePath string, handler http.Handler) (*httptest.Server, error) { cert, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) if err != nil { return nil, err } server := httptest.NewUnstartedServer(handler) server.TLS = &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, } server.StartTLS() return server, nil } // MakeSelfSignedCert generates a self-signed certificate and writes it to the specified files. // See: https://golang.org/src/crypto/tls/generate_cert.go func MakeSelfSignedCert(certFilePath, keyFilePath string) error { hosts := []string{"127.0.0.1"} isCA := true priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return err } notBefore := time.Now() notAfter := notBefore.Add(time.Hour * 24) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return err } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"Test"}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } for _, h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, h) } } if isCA { template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) if err != nil { return err } certOut, err := os.Create(certFilePath) //nolint:gosec if err != nil { return err } if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { return err } if err := certOut.Close(); err != nil { return err } keyOut, err := os.OpenFile(keyFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) //nolint:gosec if err != nil { return err } block, err := pemBlockForKey(priv) if err != nil { return err } if err := pem.Encode(keyOut, block); err != nil { return err } if err := keyOut.Close(); err != nil { return err } return nil } func pemBlockForKey(priv interface{}) (*pem.Block, error) { switch k := priv.(type) { case *rsa.PrivateKey: return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil case *ecdsa.PrivateKey: b, err := x509.MarshalECPrivateKey(k) if err != nil { return nil, err } return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil default: return nil, nil } } func publicKey(priv interface{}) interface{} { switch k := priv.(type) { case *rsa.PrivateKey: return &k.PublicKey case *ecdsa.PrivateKey: return &k.PublicKey default: return nil } } go-test-helpers-3.0.2/httphelpers/certificates_test.go000066400000000000000000000012151430344127400231340ustar00rootroot00000000000000package httphelpers import ( "crypto/tls" "crypto/x509" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSelfSignedServer(t *testing.T) { handler := HandlerWithStatus(200) WithSelfSignedServer(handler, func(server *httptest.Server, certData []byte, certs *x509.CertPool) { client := *http.DefaultClient transport := &http.Transport{} transport.TLSClientConfig = &tls.Config{RootCAs: certs} client.Transport = transport resp, err := client.Get(server.URL) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, 200, resp.StatusCode) }) } go-test-helpers-3.0.2/httphelpers/clients.go000066400000000000000000000023771430344127400211030ustar00rootroot00000000000000package httphelpers import ( "fmt" "net/http" "net/http/httptest" ) type transportFromHandler struct { handler http.Handler } func (t transportFromHandler) RoundTrip(req *http.Request) (resp *http.Response, err error) { defer func() { if r := recover(); r != nil { if thrownError, ok := r.(error); ok { err = thrownError } else { err = fmt.Errorf("error from handler: %v", r) } resp = nil } }() recorder := httptest.NewRecorder() t.handler.ServeHTTP(recorder, req) resp = recorder.Result() return } // ClientFromHandler returns an http.Client that does not do real network activity, but instead delegates // to a http.Handler as if that handler were being used by a server. // // This makes it possible to reuse the other handler-related functions in this package to control an http.Client // rather than using the somewhat less convenient RoundTripper interface. // // If the handler panics, it returns an error instead of a response. This can be used to simulate an I/O error // (since the http.Handler interface does not provide any way *not* to return an actual HTTP response). func ClientFromHandler(handler http.Handler) *http.Client { client := *http.DefaultClient client.Transport = transportFromHandler{handler} return &client } go-test-helpers-3.0.2/httphelpers/clients_test.go000066400000000000000000000014451430344127400221350ustar00rootroot00000000000000package httphelpers import ( "errors" "net/http" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClientFromHandler(t *testing.T) { handler := HandlerWithStatus(418) client := ClientFromHandler(handler) resp, err := client.Get("/") require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, 418, resp.StatusCode) } func TestClientFromHandlerConvertsPanicToError(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { panic("sorry") }) client := ClientFromHandler(handler) resp, err := client.Get("/") expectedError := &url.Error{Op: "Get", URL: "/", Err: errors.New("error from handler: sorry")} require.Error(t, err) require.Nil(t, resp) assert.Equal(t, expectedError, err) } go-test-helpers-3.0.2/httphelpers/handlers.go000066400000000000000000000153011430344127400212310ustar00rootroot00000000000000package httphelpers import ( "encoding/json" "io" "log" "net/http" "net/http/httptest" "regexp" ) // HTTPRequestInfo represents a request captured by NewRecordingHTTPHandler. type HTTPRequestInfo struct { Request *http.Request Body []byte // body has to be captured separately by server because you can't read it after the response is sent } func getRequestBody(request *http.Request) []byte { if request.Body == nil { return nil } body, _ := io.ReadAll(request.Body) return body } // DelegatingHandler is a struct that behaves as an http.Handler by delegating to the handler it wraps. // Use this if you want to change the handler's behavior dynamically during a test. // // dh := &httphelpers.DelegatingHandler{httphelpers.HandlerWithStatus(200)} // server := httptest.NewServer(dh) // the server will return 200 // dh.Handler = httphelpers.HandlerWithStatus(401) // now the server will return 401 type DelegatingHandler struct { Handler http.Handler } func (d *DelegatingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { d.Handler.ServeHTTP(w, r) } // HandlerForMethod is a simple alternative to using an HTTP router. It delegates to the specified handler // if the method matches; otherwise to the default handler, or a 405 if that is nil. func HandlerForMethod(method string, handlerForMethod http.Handler, defaultHandler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == method { handlerForMethod.ServeHTTP(w, r) } else { if defaultHandler != nil { defaultHandler.ServeHTTP(w, r) } else { w.WriteHeader(405) } } }) } // HandlerForPath is a simple alternative to using an HTTP router. It delegates to the specified handler // if the path matches; otherwise to the default handler, or a 404 if that is nil. func HandlerForPath(path string, handlerForPath http.Handler, defaultHandler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == path { handlerForPath.ServeHTTP(w, r) } else { if defaultHandler != nil { defaultHandler.ServeHTTP(w, r) } else { w.WriteHeader(404) } } }) } // HandlerForPathRegex is a simple alternative to using an HTTP router. It delegates to the specified handler // if the path matches; otherwise to the default handler, or a 404 if that is nil. func HandlerForPathRegex(pathRegex string, handlerForPath http.Handler, defaultHandler http.Handler) http.Handler { pr := regexp.MustCompile(pathRegex) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if pr.MatchString(r.URL.Path) { handlerForPath.ServeHTTP(w, r) } else { if defaultHandler != nil { defaultHandler.ServeHTTP(w, r) } else { w.WriteHeader(404) } } }) } // HandlerWithJSONResponse creates an HTTP handler that returns a 200 status and the JSON encoding of // the specified object. func HandlerWithJSONResponse(contentToEncode interface{}, additionalHeaders http.Header) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { bytes, err := json.Marshal(contentToEncode) if err != nil { log.Printf("error encoding JSON response: %s", err) w.WriteHeader(500) } else { w.Header().Set("Content-Type", "application/json") for k, vv := range additionalHeaders { w.Header()[k] = vv } w.WriteHeader(200) _, _ = w.Write(bytes) } }) } // HandlerWithResponse creates an HTTP handler that always returns the same status code, headers, and body. func HandlerWithResponse(status int, headers http.Header, body []byte) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for k, vv := range headers { w.Header()[k] = vv } w.WriteHeader(status) if body != nil { _, _ = w.Write(body) } }) } // HandlerWithStatus creates an HTTP handler that always returns the same status code. func HandlerWithStatus(status int) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status) }) } // RecordingHandler wraps any HTTP handler in another handler that pushes received requests onto a channel. // // handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(200)) // httphelpers.WithServer(handler, func(server *http.TestServer) { // doSomethingThatMakesARequest(server.URL) // request will receive a 200 status // r := <-requestsCh // verifyRequestPropertiesWereCorrect(r.Request, r.Body) // }) func RecordingHandler(delegateToHandler http.Handler) (http.Handler, <-chan HTTPRequestInfo) { requestsCh := make(chan HTTPRequestInfo, 100) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestsCh <- HTTPRequestInfo{r, getRequestBody(r)} delegateToHandler.ServeHTTP(w, r) }) return handler, requestsCh } // SequentialHandler creates an HTTP handler that delegates to one handler per request, in the order given. // If there are more requests than parameters, all subsequent requests go to the last handler. // // In this example, the first HTTP request will get a 503, and all subsequent requests will get a 200. // // handler := httphelpers.SequentialHandler( // httphelpers.HandlerWithStatus(503), // httphelpers.HandlerWithStatus(200) // ) func SequentialHandler(firstHandler http.Handler, remainingHandlers ...http.Handler) http.Handler { allHandlers := append([]http.Handler{firstHandler}, remainingHandlers...) requestCounter := 0 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := allHandlers[requestCounter] if requestCounter < len(allHandlers)-1 { requestCounter++ } handler.ServeHTTP(w, r) }) } // BrokenConnectionHandler creates an HTTP handler that will simulate an I/O error. // // When used with an httptest.Server, the handler forces an early close of the connection. // When used in a client created with ClientFromHandler, it causes a panic which is recovered // and converted to an error result. However, do not use this with httptest.ResponseRecorder // or your test will panic. // // handler := BrokenConnectionHandler() // client := NewClientFromHandler(handler) // // All requests made with this client will return an error func BrokenConnectionHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, ok := w.(*httptest.ResponseRecorder); ok { panic("httphelpers.BrokenConnectionHandler cannot be used with a ResponseRecorder") } if h, ok := w.(http.Hijacker); ok { conn, _, err := h.Hijack() if err == nil { _ = conn.Close() return } } panic("connection deliberately closed by httphelpers.BrokenConnectionHandler; a panic stacktrace" + " here from the Go HTTP framework is expected and can be ignored") }) } go-test-helpers-3.0.2/httphelpers/handlers_sse.go000066400000000000000000000102131430344127400221000ustar00rootroot00000000000000package httphelpers import ( "bytes" "fmt" "net/http" ) // SSEEvent is a simple representation of a Server-Sent Events message. type SSEEvent struct { // ID is the optional unique ID of the event. ID string // Event is the message type, if any. Event string // Data is the message data. Data string // RetryMillis is an optional field that changes the client's reconnection delay to the specified number // of milliseconds. If zero or negative, the field will not be sent. RetryMillis int } // Bytes returns the stream data for the event. func (e SSEEvent) Bytes() []byte { var buf bytes.Buffer if e.ID != "" { buf.WriteString(fmt.Sprintf("id: %s\n", e.ID)) } if e.Event != "" { buf.WriteString(fmt.Sprintf("event: %s\n", e.Event)) } if e.RetryMillis > 0 { buf.WriteString(fmt.Sprintf("retry: %d\n", e.RetryMillis)) } buf.WriteString(fmt.Sprintf("data: %s\n\n", e.Data)) return buf.Bytes() } // SSEStreamControl is the interface for manipulating streams created by SSEHandler. type SSEStreamControl interface { // Enqueue is the same as Send, except that if there are currently no open connections to this // endpoint, the event is enqueued and will be sent to the next client that connects. Enqueue(event SSEEvent) // Send sends an SSE event. If there are multiple open connections to this endpoint, the same // event is sent to all of them. If there are no open connections, the event is discarded. Send(event SSEEvent) // EnqueueComment is the same as Enqueue, except that it sends a comment line instead of an // event. A colon is prepended to the comment. EnqueueComment(comment string) // SendComment is the same as Send, except that it sends a comment line instead of an event. // A colon is prepended to the comment. SendComment(comment string) // EndAll terminates any existing connections to this endpoint, but allows new connections // afterward. EndAll() // Close terminates any existing connections to this endpoint and causes the handler to reject any // subsequent connections. Close() error } type sseStreamControlImpl struct { streamControl StreamControl } // SSEHandler creates an HTTP handler that streams Server-Sent Events data. // // The initialData parameter, if not nil, specifies a starting event that should always be sent to any // client that has connected to this endpoint. Then, any data provided via the SSEStreamControl interface // is copied to all connected clients. Connections remain open until either EndAll or Close is called on // on the SSEStreamControl. // // In this example, every request to this endpoint will receive an initial initEvent, and then another // event will be sent every second with a counter; every 30 seconds, all active stream connections are // are closed: // // initialEvent := httphelpers.SSEEvent{Data: "hello"} // handler, stream := httphelpers.SSEHandler(&initialEvent) // (start server with handler) // go func() { // n := 1 // counter := time.NewTicker(time.Second) // interrupter := time.NewTicker(time.Second * 10) // for { // select { // case <-counter.C: // stream.Send(httphelpers.SSEEvent{Data: fmt.Sprintf("%d\n", n)}) // n++ // case <-interrupter.C: // stream.EndAll() // } // } // }() func SSEHandler(initialEvent *SSEEvent) (http.Handler, SSEStreamControl) { var initialData []byte if initialEvent != nil { initialData = initialEvent.Bytes() } handler, streamControl := ChunkedStreamingHandler(initialData, "text/event-stream; charset=utf-8") return handler, &sseStreamControlImpl{streamControl} } func (s *sseStreamControlImpl) Enqueue(event SSEEvent) { s.streamControl.Enqueue(event.Bytes()) } func (s *sseStreamControlImpl) Send(event SSEEvent) { s.streamControl.Send(event.Bytes()) } func (s *sseStreamControlImpl) EnqueueComment(comment string) { s.streamControl.Enqueue([]byte(fmt.Sprintf(":%s\n", comment))) } func (s *sseStreamControlImpl) SendComment(comment string) { s.streamControl.Send([]byte(fmt.Sprintf(":%s\n", comment))) } func (s *sseStreamControlImpl) EndAll() { s.streamControl.EndAll() } func (s *sseStreamControlImpl) Close() error { return s.streamControl.Close() } go-test-helpers-3.0.2/httphelpers/handlers_sse_test.go000066400000000000000000000021341430344127400231420ustar00rootroot00000000000000package httphelpers import ( "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSSEHandler(t *testing.T) { initialEvent := SSEEvent{"id1", "event1", "data1", 0} handler, stream := SSEHandler(&initialEvent) defer stream.Close() stream.Enqueue(SSEEvent{"", "event2", "data2", 0}) stream.EnqueueComment("comment1") stream.Send(SSEEvent{"", "", "this isn't sent because there are no connections", 0}) WithServer(handler, func(server *httptest.Server) { resp1, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp1.Body.Close() assert.Equal(t, 200, resp1.StatusCode) assert.Equal(t, "text/event-stream; charset=utf-8", resp1.Header.Get("Content-Type")) stream.SendComment("comment2") stream.Enqueue(SSEEvent{"", "event3", "data3", 500}) stream.EndAll() data, err := io.ReadAll(resp1.Body) assert.NoError(t, err) assert.Equal(t, `id: id1 event: event1 data: data1 event: event2 data: data2 :comment1 :comment2 event: event3 retry: 500 data: data3 `, string(data)) }) } go-test-helpers-3.0.2/httphelpers/handlers_streaming.go000066400000000000000000000126651430344127400233140ustar00rootroot00000000000000package httphelpers import ( "log" "net/http" "sync" ) // StreamControl is the interface for manipulating streams created by ChunkedStreamingHandler. type StreamControl interface { // Enqueue is the same as Send, except that if there are currently no open connections to this // endpoint, the data is enqueued and will be sent to the next client that connects. Enqueue(data []byte) // Send sends a chunk of data. If there are multiple open connections to this endpoint, the same // data is sent to all of them. If there are no open connections, the data is discarded. Send(data []byte) // EndAll terminates any existing connections to this endpoint, but allows new connections // afterward. EndAll() // Close terminates any existing connections to this endpoint and causes the handler to reject any // subsequent connections. Close() error } // ChunkedStreamingHandler creates an HTTP handler that streams arbitrary data using chunked encoding. // // The initialData parameter, if not nil, specifies a starting chunk that should always be sent to any // client that has connected to this endpoint. Then, any data provided via the StreamControl interface // is copied to all connected clients. Connections remain open until either EndAll or Close is called // on the StreamControl. // // In this example, every request to this endpoint will receive an initial message of "hello\n", and // then another line will be sent every second with a counter; every 30 seconds, all active stream // connections are closed: // // handler, stream := httphelpers.ChunkedStreamingHandler([]byte("hello\n"), "text/plain") // (start server with handler) // go func() { // n := 1 // counter := time.NewTicker(time.Second) // interrupter := time.NewTicker(time.Second * 10) // for { // select { // case <-counter.C: // stream.Send([]byte(fmt.Sprintf("%d\n", n))) // n++ // case <-interrupter.C: // stream.EndAll() // } // } // }() func ChunkedStreamingHandler(initialChunk []byte, contentType string) (http.Handler, StreamControl) { sh := &chunkedStreamingHandlerImpl{ initialChunk: initialChunk, contentType: contentType, } return sh, sh } type chunkedStreamingHandlerImpl struct { initialChunk []byte contentType string queued [][]byte channels []chan []byte closed bool lock sync.Mutex } func (s *chunkedStreamingHandlerImpl) Enqueue(data []byte) { s.sendInternal(data, true) } func (s *chunkedStreamingHandlerImpl) Send(data []byte) { s.sendInternal(data, false) } func (s *chunkedStreamingHandlerImpl) EndAll() { s.endAllInternal(false) } func (s *chunkedStreamingHandlerImpl) Close() error { s.endAllInternal(true) return nil } func (s *chunkedStreamingHandlerImpl) sendInternal(data []byte, enqueueIfNoChannels bool) { if len(data) == 0 { // In chunked encoding, a zero-length chunk terminates the response. We don't want the caller to // do that by accident, so we require that they call EndAll or Close instead. return } s.lock.Lock() defer s.lock.Unlock() if s.closed { return } if len(s.channels) == 0 { if enqueueIfNoChannels { s.queued = append(s.queued, data) } return } for _, ch := range s.channels { ch <- data } } func (s *chunkedStreamingHandlerImpl) endAllInternal(thenClose bool) { s.lock.Lock() if thenClose { s.closed = true } channels := s.channels s.channels = nil s.lock.Unlock() for _, ch := range channels { close(ch) } } func (s *chunkedStreamingHandlerImpl) removeChannel(channelToRemove chan []byte) { // This is called when the client closed the connection. go func() { // Consume anything else that gets sent on this channel, until it's closed, to avoid deadlock for range channelToRemove { } }() s.lock.Lock() for i, ch := range s.channels { if ch == channelToRemove { copy(s.channels[i:], s.channels[i+1:]) s.channels[len(s.channels)-1] = nil s.channels = s.channels[:len(s.channels)-1] break } } s.lock.Unlock() // At this point, no one else will ever see this channel, so it's safe to close close(channelToRemove) } func (s *chunkedStreamingHandlerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { log.Println("httphelpers.ChunkedStreamingHandler can't be used with a ResponseWriter that does not support Flush") w.WriteHeader(500) return } s.lock.Lock() if s.closed { log.Println("httphelpers.ChunkedStreamingHandler received a request after it was closed") w.WriteHeader(500) s.lock.Unlock() return } dataCh := make(chan []byte, 10) s.channels = append(s.channels, dataCh) queued := s.queued s.queued = nil s.lock.Unlock() h := w.Header() h.Set("Content-Type", s.contentType) h.Set("Cache-Control", "no-cache, no-store, must-revalidate") if s.initialChunk != nil { _, _ = w.Write(s.initialChunk) flusher.Flush() } for _, data := range queued { _, _ = w.Write(data) flusher.Flush() } flusher.Flush() var closeNotifyCh <-chan bool // CloseNotifier is deprecated but there's no way to use Context in this case if closeNotifier, ok := w.(http.CloseNotifier); ok { //nolint:megacheck closeNotifyCh = closeNotifier.CloseNotify() } StreamLoop: for { select { case data, ok := <-dataCh: if !ok { // closed break StreamLoop } _, _ = w.Write(data) flusher.Flush() case <-closeNotifyCh: // client has closed the connection s.removeChannel(dataCh) break StreamLoop } } } go-test-helpers-3.0.2/httphelpers/handlers_streaming_test.go000066400000000000000000000066471430344127400243560ustar00rootroot00000000000000package httphelpers import ( "io" "net/http" "net/http/httptest" "testing" "time" helpers "github.com/launchdarkly/go-test-helpers/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestChunkedStreamingHandlerReturnsResponseBeforeFirstData(t *testing.T) { handler, stream := ChunkedStreamingHandler(nil, "text/plain") defer stream.Close() WithServer(handler, func(server *httptest.Server) { resp, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) }) } func TestChunkedStreamingHandlerSend(t *testing.T) { initialData := []byte("hello,") handler, stream := ChunkedStreamingHandler(initialData, "text/plain") defer stream.Close() stream.Enqueue([]byte("first,")) stream.Send([]byte("this isn't sent because there are no connections")) stream.Enqueue([]byte("second,")) WithServer(handler, func(server *httptest.Server) { resp1, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp1.Body.Close() assert.Equal(t, 200, resp1.StatusCode) assert.Equal(t, "text/plain", resp1.Header.Get("Content-Type")) stream.Send(nil) // should have no effect stream.Send(make([]byte, 0)) // also no effect stream.Send([]byte("third,")) expected := "hello,first,second,third," assert.Equal(t, expected, string(helpers.ReadWithTimeout(resp1.Body, len(expected), time.Second))) resp2, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp2.Body.Close() expected = "hello," assert.Equal(t, expected, string(helpers.ReadWithTimeout(resp2.Body, len(expected), time.Second))) stream.Send([]byte("fourth.")) expected = "fourth." assert.Equal(t, expected, string(helpers.ReadWithTimeout(resp1.Body, len(expected), time.Second))) assert.Equal(t, expected, string(helpers.ReadWithTimeout(resp2.Body, len(expected), time.Second))) }) } func TestChunkedStreamingHandlerEndAll(t *testing.T) { initialData := []byte("hello,") handler, stream := ChunkedStreamingHandler(initialData, "text/plain") defer stream.Close() WithServer(handler, func(server *httptest.Server) { resp1, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp1.Body.Close() go func() { stream.Send([]byte("goodbye.")) stream.EndAll() }() // ReadAll won't return until the stream is closed data, err := io.ReadAll(resp1.Body) require.NoError(t, err) assert.Equal(t, "hello,goodbye.", string(data)) resp2, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp2.Body.Close() go func() { stream.EndAll() }() data, err = io.ReadAll(resp2.Body) require.NoError(t, err) assert.Equal(t, "hello,", string(data)) }) } func TestChunkedStreamingHandlerClose(t *testing.T) { initialData := []byte("hello,") handler, stream := ChunkedStreamingHandler(initialData, "text/plain") defer stream.Close() WithServer(handler, func(server *httptest.Server) { resp1, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) defer resp1.Body.Close() go func() { stream.Send([]byte("goodbye.")) stream.Close() }() data, err := io.ReadAll(resp1.Body) require.NoError(t, err) assert.Equal(t, "hello,goodbye.", string(data)) // Should error out on any further requests resp2, err := http.DefaultClient.Get(server.URL) require.NoError(t, err) assert.Equal(t, 500, resp2.StatusCode) }) } go-test-helpers-3.0.2/httphelpers/handlers_test.go000066400000000000000000000125251430344127400222750ustar00rootroot00000000000000package httphelpers import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestDelegatingHandler(t *testing.T) { h1 := HandlerWithStatus(200) h2 := HandlerWithStatus(304) dh := DelegatingHandler{h1} rr1 := httptest.NewRecorder() req1, _ := http.NewRequest("GET", "/", nil) dh.ServeHTTP(rr1, req1) assert.Equal(t, 200, rr1.Code) dh.Handler = h2 rr2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/", nil) dh.ServeHTTP(rr2, req2) assert.Equal(t, 304, rr2.Code) } func TestHandlerForMethod(t *testing.T) { h1 := HandlerWithStatus(200) h2 := HandlerWithStatus(202) hm := HandlerForMethod("GET", h1, HandlerForMethod("POST", h2, nil)) rr1 := httptest.NewRecorder() req1, _ := http.NewRequest("GET", "/", nil) hm.ServeHTTP(rr1, req1) assert.Equal(t, 200, rr1.Code) rr2 := httptest.NewRecorder() req2, _ := http.NewRequest("POST", "/", nil) hm.ServeHTTP(rr2, req2) assert.Equal(t, 202, rr2.Code) rr3 := httptest.NewRecorder() req3, _ := http.NewRequest("PATCH", "/", nil) hm.ServeHTTP(rr3, req3) assert.Equal(t, 405, rr3.Code) } func TestHandlerForPath(t *testing.T) { h1 := HandlerWithStatus(200) h2 := HandlerWithStatus(304) hp := HandlerForPath("/path1", h1, HandlerForPath("/path2", h2, nil)) rr1 := httptest.NewRecorder() req1, _ := http.NewRequest("GET", "/path1", nil) hp.ServeHTTP(rr1, req1) assert.Equal(t, 200, rr1.Code) rr2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/path2", nil) hp.ServeHTTP(rr2, req2) assert.Equal(t, 304, rr2.Code) rr3 := httptest.NewRecorder() req3, _ := http.NewRequest("GET", "/path3", nil) hp.ServeHTTP(rr3, req3) assert.Equal(t, 404, rr3.Code) } func TestHandlerForPathRegex(t *testing.T) { h1 := HandlerWithStatus(200) h2 := HandlerWithStatus(304) hp := HandlerForPathRegex("^/path[12]$", h1, h2) rr1 := httptest.NewRecorder() req1, _ := http.NewRequest("GET", "/path1", nil) hp.ServeHTTP(rr1, req1) assert.Equal(t, 200, rr1.Code) rr2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/path2", nil) hp.ServeHTTP(rr2, req2) assert.Equal(t, 200, rr2.Code) rr3 := httptest.NewRecorder() req3, _ := http.NewRequest("GET", "/path3", nil) hp.ServeHTTP(rr3, req3) assert.Equal(t, 304, rr3.Code) } func TestHandlerWithJSONResponse(t *testing.T) { jsonObject := map[string]string{"things": "stuff"} headers := make(http.Header) headers.Set("X-My-Header", "hello") h := HandlerWithJSONResponse(jsonObject, headers) rr := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(rr, req) assert.Equal(t, 200, rr.Code) assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) assert.Equal(t, "hello", rr.Header().Get("X-My-Header")) assert.Equal(t, []byte(`{"things":"stuff"}`), rr.Body.Bytes()) } func TestHandlerWithResponse(t *testing.T) { headers1 := make(http.Header) headers1.Set("X-My-Header", "hello") h1 := HandlerWithResponse(200, headers1, nil) rr1 := httptest.NewRecorder() req1, _ := http.NewRequest("GET", "/", nil) h1.ServeHTTP(rr1, req1) assert.Equal(t, 200, rr1.Code) assert.Equal(t, "hello", rr1.Header().Get("X-My-Header")) assert.Nil(t, rr1.Body.Bytes()) headers2 := make(http.Header) headers2.Set("Content-Type", "text/plain") h2 := HandlerWithResponse(200, headers2, []byte("hello")) rr2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/", nil) h2.ServeHTTP(rr2, req2) assert.Equal(t, 200, rr2.Code) assert.Equal(t, "text/plain", rr2.Header().Get("Content-Type")) assert.Equal(t, []byte("hello"), rr2.Body.Bytes()) } func TestHandlerWithStatus(t *testing.T) { h := HandlerWithStatus(418) // I'm a teapot rr := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(rr, req) assert.Equal(t, 418, rr.Code) } func TestRecordingHandler(t *testing.T) { h := HandlerWithStatus(418) rh, requestsCh := RecordingHandler(h) req1, _ := http.NewRequest("GET", "/1", nil) rr1 := httptest.NewRecorder() rh.ServeHTTP(rr1, req1) postData := []byte("hello") req2, _ := http.NewRequest("GET", "/2", bytes.NewBuffer(postData)) rr2 := httptest.NewRecorder() rh.ServeHTTP(rr2, req2) assert.Equal(t, 418, rr1.Code) assert.Equal(t, 418, rr2.Code) assert.Equal(t, 2, len(requestsCh)) ri1 := <-requestsCh assert.Equal(t, req1.URL.Path, ri1.Request.URL.Path) assert.Nil(t, ri1.Body) ri2 := <-requestsCh assert.Equal(t, req2.URL.Path, ri2.Request.URL.Path) assert.Equal(t, postData, ri2.Body) } func TestSequentialHandler(t *testing.T) { h1 := HandlerWithStatus(500) h2 := HandlerWithStatus(400) sh := SequentialHandler(h1, h2) req1, _ := http.NewRequest("GET", "/1", nil) rr1 := httptest.NewRecorder() sh.ServeHTTP(rr1, req1) req2, _ := http.NewRequest("GET", "/2", nil) rr2 := httptest.NewRecorder() sh.ServeHTTP(rr2, req2) req3, _ := http.NewRequest("GET", "/3", nil) rr3 := httptest.NewRecorder() sh.ServeHTTP(rr3, req3) assert.Equal(t, 500, rr1.Code) assert.Equal(t, 400, rr2.Code) assert.Equal(t, 400, rr3.Code) } func TestBrokenConnectionHandler(t *testing.T) { h := BrokenConnectionHandler() t.Run("with instrumented client", func(t *testing.T) { client := ClientFromHandler(h) _, err := client.Get("/") assert.Error(t, err) }) t.Run("with server", func(t *testing.T) { WithServer(h, func(server *httptest.Server) { _, err := http.DefaultClient.Get(server.URL) assert.Error(t, err) }) }) } go-test-helpers-3.0.2/httphelpers/package_info.go000066400000000000000000000001441430344127400220360ustar00rootroot00000000000000// Package httphelpers contains convenience tools for testing HTTP client code. package httphelpers go-test-helpers-3.0.2/httphelpers/servers.go000066400000000000000000000006461430344127400211300ustar00rootroot00000000000000package httphelpers import ( "net/http" "net/http/httptest" ) // WithServer creates an httptest.Server from the given handler, passes the server instance to the given // function, and ensures that the server is closed afterward. func WithServer(handler http.Handler, action func(*httptest.Server)) { server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() action(server) } go-test-helpers-3.0.2/httphelpers/servers_test.go000066400000000000000000000010461430344127400221620ustar00rootroot00000000000000package httphelpers import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWithServer(t *testing.T) { handler := HandlerWithStatus(200) var url string WithServer(handler, func(server *httptest.Server) { url = server.URL resp, err := http.DefaultClient.Get(url) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, 200, resp.StatusCode) }) _, err := http.DefaultClient.Get(url) require.Error(t, err) // server is no longer listening } go-test-helpers-3.0.2/io.go000066400000000000000000000015561430344127400155050ustar00rootroot00000000000000package helpers import ( "io" "time" ) // ReadWithTimeout reads data until it gets the desired number of bytes or times out. // // This is an inefficient implementation that should only be used in tests. func ReadWithTimeout(r io.Reader, n int, timeout time.Duration) []byte { byteCh := make(chan byte) closer := make(chan struct{}) go func() { count := 0 for { select { case <-closer: return default: b := make([]byte, 1) got, err := r.Read(b) if err != nil { return } if got > 0 { byteCh <- b[0] } count++ if count >= n { return } } } }() buf := make([]byte, 0, n) deadline := time.After(timeout) ReadLoop: for { select { case b := <-byteCh: buf = append(buf, b) if len(buf) >= n { break ReadLoop } case <-deadline: break ReadLoop } } close(closer) return buf } go-test-helpers-3.0.2/io_test.go000066400000000000000000000016711430344127400165420ustar00rootroot00000000000000package helpers import ( "bytes" "io" "testing" "time" "github.com/stretchr/testify/assert" ) func TestReadWithTimeout(t *testing.T) { var buf1 bytes.Buffer data := ReadWithTimeout(&buf1, 5, time.Millisecond*50) assert.Len(t, data, 0) var buf2 bytes.Buffer buf2.WriteString("he") data = ReadWithTimeout(&buf2, 5, time.Millisecond*50) assert.Equal(t, "he", string(data)) var buf3 bytes.Buffer buf3.WriteString("hello") data = ReadWithTimeout(&buf3, 5, time.Millisecond*50) assert.Equal(t, "hello", string(data)) r1, w1 := io.Pipe() go func() { w1.Write([]byte("good")) time.Sleep(10 * time.Millisecond) w1.Write([]byte("bye")) }() data = ReadWithTimeout(r1, 7, time.Millisecond*100) assert.Equal(t, "goodbye", string(data)) r2, w2 := io.Pipe() go func() { w2.Write([]byte("good")) }() data = ReadWithTimeout(r2, 7, time.Millisecond*100) time.Sleep(time.Millisecond * 100) assert.Equal(t, "good", string(data)) } go-test-helpers-3.0.2/jsonhelpers/000077500000000000000000000000001430344127400170745ustar00rootroot00000000000000go-test-helpers-3.0.2/jsonhelpers/assertions.go000066400000000000000000000022351430344127400216170ustar00rootroot00000000000000package jsonhelpers import ( "reflect" "strings" "github.com/stretchr/testify/assert" ) // AssertEqual compares two JSON Value instances and returns true if they are deeply equal. // If they are not equal, it outputs a test failure message describing the mismatch as // specifically as possible. // // The two values may either be pre-parsed JValue instances, or if they are not, they are // converted using the same rules as JValueOf. func AssertEqual(t assert.TestingT, expected, actual any) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } ev, av := JValueOf(expected), JValueOf(actual) if ev.err != nil { t.Errorf("invalid expected value (%s): %s", ev.err, ev) return false } if av.err != nil { t.Errorf("invalid actual value (%s): %s", av.err, av) return false } if reflect.DeepEqual(ev.parsed, av.parsed) { return true } diff := describeValueDifference(ev.parsed, av.parsed, nil) if len(diff) == 1 && diff[0].Path == nil { t.Errorf("expected JSON value: %s\nactual value: %s", expected, actual) } else { t.Errorf("incorrect JSON value: %s\n"+strings.Join(diff.Describe("expected", "actual"), "\n"), actual) } return false } go-test-helpers-3.0.2/jsonhelpers/assertions_test.go000066400000000000000000000024141430344127400226550ustar00rootroot00000000000000package jsonhelpers import ( "testing" "github.com/launchdarkly/go-test-helpers/v3/testbox" "github.com/stretchr/testify/assert" ) func TestAssertEqual(t *testing.T) { AssertEqual(t, `{"a":true,"b":false}`, `{"b":false,"a":true}`) AssertEqual(t, JValueOf(`{"a":true,"b":false}`), JValueOf(`{"b":false,"a":true}`)) result := testbox.SandboxTest(func(t testbox.TestingT) { AssertEqual(t, `{"a":true,"b":false}`, `{"a":false,"b":false}`) }) assert.True(t, result.Failed) if assert.Len(t, result.Failures, 1) { assert.Equal(t, `incorrect JSON value: {"a":false,"b":false} at "a": expected = true, actual = false`, result.Failures[0].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { AssertEqual(t, `{"a":true,"b":false}`, `{`) }) assert.True(t, result.Failed) if assert.Len(t, result.Failures, 1) { assert.Equal(t, `invalid actual value (JSON unmarshaling error: unexpected end of JSON input): {`, result.Failures[0].Message) } result = testbox.SandboxTest(func(t testbox.TestingT) { AssertEqual(t, `{`, `{"a":true,"b":false}`) }) assert.True(t, result.Failed) if assert.Len(t, result.Failures, 1) { assert.Equal(t, `invalid expected value (JSON unmarshaling error: unexpected end of JSON input): {`, result.Failures[0].Message) } } go-test-helpers-3.0.2/jsonhelpers/diff_json.go000066400000000000000000000175631430344127400214000ustar00rootroot00000000000000package jsonhelpers import ( "encoding/json" "fmt" "reflect" "sort" "strings" ) // JSONDiffResult is a list of JSONDiffElement values returned by JSONDiff. type JSONDiffResult []JSONDiffElement // Describe returns a list of string descriptions of the differences. func (r JSONDiffResult) Describe(value1Name, value2Name string) []string { ret := make([]string, 0, len(r)) for _, e := range r { ret = append(ret, e.Describe(value1Name, value2Name)) } return ret } // JSONDiffElement describes a point of difference between two JSON data structures. type JSONDiffElement struct { // Path represents the location of the data as a path from the root. Path JSONPath // Value1 is the JSON encoding of the value at that path in the data structure // that was passed to JSONDiff as json1. An empty string (as opposed to the JSON // representation of an empty string, `""`) means that this property was missing // in json1. Value1 string // Value2 is the JSON encoding of the value at that path in the data structure // that was passed to JSONDiff as json2. An empty string (as opposed to the JSON // representation of an empty string, `""`) means that this property was missing // in json2. Value2 string } // Describe returns a string description of this difference. func (e JSONDiffElement) Describe(value1Name, value2Name string) string { var desc1, desc2 = e.Value1, e.Value2 if desc1 == "" { desc1 = "" } if desc2 == "" { desc2 = "" } pathPrefix := "" if len(e.Path) != 0 { pathPrefix = fmt.Sprintf("at %s: ", e.Path) } return fmt.Sprintf("%s%s = %s, %s = %s", pathPrefix, value1Name, desc1, value2Name, desc2) } // JSONPath represents the location of a node in a JSON data structure. // // In a JSON object {"a":{"b":2}}, the nested "b":2 property would be referenced as // JSONPath{{Property: "a"}, {Property: "b"}}. // // In a JSON array ["a","b",["c"]], the "c" value would be referenced as // JSONPath{{Index: 2},{Index: 0}}. // // A nil or zero-length slice represents the root of the data. type JSONPath []JSONPathComponent // String returns a string representation of the path. func (p JSONPath) String() string { parts := make([]string, 0, len(p)) for _, c := range p { if c.Property == "" { parts = append(parts, fmt.Sprintf("[%d]", c.Index)) } else { parts = append(parts, fmt.Sprintf(`"%s"`, c.Property)) } } return strings.Join(parts, ".") } // JSONPathComponent represents a location within the top level of a JSON object or array. type JSONPathComponent struct { // Property is the name of an object property, or "" if this is in an array. Property string // Index is the zero-based index of an array element, if this is in an array. Index int } // JSONDiff compares two JSON values and returns an explanation of how they differ, if at all, // ignoring any differences that do not affect the value semantically (such as whitespace). // This is for programmatic use; if you want a human-readable test assertion based on the // same logic, see matchers.JSONEqual (which calls this function). // // The two values are provided as marshalled JSON data. If they cannot be parsed, the // function immediately returns an error. // // If the values are deeply equal, the result is nil. // // Otherwise, if they are both simple values, the result will contain a single // JSONDiffElement. // // If they are both JSON objects, JSONDiff will compare their properties. It will produce // a JSONDiffElement for each property where they differ. For instance, comparing // {"a": 1, "b": 2} with {"a": 1, "b": 3, "c": 4} will produce one element for "b" and // one for "c". If a property contains an object value on both sides, the comparison will // proceed recursively and may produce elements with subpaths (see JSONPath). // // If they are both JSON arrays, and are of the same length, JSONDiff will compare their // elements using the same rules as above. For JSON arrays of different lengths, if the // shorter one matches every corresponding element of the longer one, it will return a // JSONDiffElement pointing to the first element after the shorter one and listing the // additional elements starting with a comma (for instance, comparing [10,20] with // [10,20,30] will return a string of ",30" at index 2); otherwise it will just return // both arrays in their entirety. // // Values that are not of the same type will always produce a single JSONDiffElement // describing the entire values. func JSONDiff(json1, json2 []byte) (JSONDiffResult, error) { var value1, value2 interface{} if err := json.Unmarshal(json1, &value1); err != nil { return nil, err } if err := json.Unmarshal(json2, &value2); err != nil { return nil, err } return describeValueDifference(value1, value2, nil), nil } func describeValueDifference(value1, value2 interface{}, path JSONPath) JSONDiffResult { if a1, ok := value1.([]interface{}); ok { if a2, ok := value2.([]interface{}); ok { return describeArrayValueDifference(a1, a2, path) } } if o1, ok := value1.(map[string]interface{}); ok { if o2, ok := value2.(map[string]interface{}); ok { return describeObjectValueDifference(o1, o2, path) } } if reflect.DeepEqual(value1, value2) { return nil } return JSONDiffResult{ {Path: path, Value1: ToJSONString(value1), Value2: ToJSONString(value2)}, } } func describeArrayValueDifference(array1, array2 []interface{}, path JSONPath) JSONDiffResult { if len(array1) != len(array2) { // Check for the case where one is a shorter version of the other but the same up to that point if len(array1) != 0 && len(array2) != 0 { shortestCommonLength := len(array1) if shortestCommonLength > len(array2) { shortestCommonLength = len(array2) } foundUnequal := false for i := 0; i < shortestCommonLength; i++ { if !reflect.DeepEqual(array1[i], array2[i]) { foundUnequal = true break } } if !foundUnequal { var remainder []interface{} if len(array1) == shortestCommonLength { remainder = array2[shortestCommonLength:] } else { remainder = array1[shortestCommonLength:] } remainderStr := ToJSONString(remainder) remainderStr = "," + strings.TrimSuffix(strings.TrimPrefix(remainderStr, "["), "]") ret := JSONDiffElement{ Path: append(append(JSONPath(nil), path...), JSONPathComponent{Index: shortestCommonLength}), } if len(array1) == shortestCommonLength { ret.Value2 = remainderStr } else { ret.Value1 = remainderStr } return JSONDiffResult{ret} } } return JSONDiffResult{ {Path: path, Value1: ToJSONString(array1), Value2: ToJSONString(array2)}, } } var diffs JSONDiffResult for i, value1 := range array1 { subpath := append(append(JSONPath(nil), path...), JSONPathComponent{Index: i}) value2 := array2[i] diffs = append(diffs, describeValueDifference(value1, value2, subpath)...) } return diffs } func describeObjectValueDifference(object1, object2 map[string]interface{}, path JSONPath) JSONDiffResult { allKeys := make(map[string]struct{}) for key := range object1 { allKeys[key] = struct{}{} } for key := range object2 { allKeys[key] = struct{}{} } allSortedKeys := make([]string, 0, len(allKeys)) for key := range allKeys { allSortedKeys = append(allSortedKeys, key) } sort.Strings(allSortedKeys) var diffs JSONDiffResult //nolint:prealloc for _, key := range allSortedKeys { subpath := append(append(JSONPath(nil), path...), JSONPathComponent{Property: key}) var desc1, desc2 = "", "" if value1, ok := object1[key]; ok { if value2, ok := object2[key]; ok { if reflect.DeepEqual(value1, value2) { continue } diffs = append(diffs, describeValueDifference(value1, value2, subpath)...) continue } else { desc1 = string(CanonicalizeJSON(ToJSON(value1))) } } else { desc2 = string(CanonicalizeJSON(ToJSON(object2[key]))) } diffs = append(diffs, JSONDiffElement{ Path: subpath, Value1: desc1, Value2: desc2, }) } return diffs } go-test-helpers-3.0.2/jsonhelpers/diff_json_test.go000066400000000000000000000074511430344127400224320ustar00rootroot00000000000000package jsonhelpers import ( "encoding/json" "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestJSONDiff(t *testing.T) { diffResult := func(t *testing.T, value1, value2 []byte) JSONDiffResult { diff, err := JSONDiff(value1, value2) assert.NoError(t, err) return diff } t.Run("equality and inequality without detailed diff", func(t *testing.T) { values := []interface{}{ nil, true, false, 3, 3.5, "x", []string{"a", "b"}, map[string]interface{}{"a": []int{1, 2}}, } for i, value1 := range values { jsonValue1, _ := json.Marshal(value1) t.Run(fmt.Sprintf("%s == %s", string(jsonValue1), string(jsonValue1)), func(t *testing.T) { assert.Nil(t, diffResult(t, jsonValue1, jsonValue1)) }) for j, value2 := range values { if j == i { continue } jsonValue2, _ := json.Marshal(value2) t.Run(fmt.Sprintf("%s != %s", string(jsonValue1), string(jsonValue2)), func(t *testing.T) { diff := diffResult(t, jsonValue1, jsonValue2) assert.Len(t, diff, 1) assert.Nil(t, diff[0].Path) assert.Equal(t, string(jsonValue1), diff[0].Value1) assert.Equal(t, string(jsonValue2), diff[0].Value2) }) } } }) t.Run("inequality with object diff", func(t *testing.T) { assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Property: "b"}}, Value1: "2", Value2: "3"}, }, diffResult(t, []byte(`{"a":1,"b":2}`), []byte(`{"a":1,"b":3}`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Property: "b"}}, Value1: "2", Value2: ""}, }, diffResult(t, []byte(`{"a":1,"b":2}`), []byte(`{"a":1}`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Property: "b"}}, Value1: "", Value2: "2"}, }, diffResult(t, []byte(`{"a":1}`), []byte(`{"a":1,"b":2}`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Property: "b"}, {Property: "c"}}, Value1: "2", Value2: "3"}, }, diffResult(t, []byte(`{"a":1,"b":{"c":2}}`), []byte(`{"a":1,"b":{"c":3}}`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Property: "b"}, {Index: 1}}, Value1: `"d"`, Value2: `"e"`}, }, diffResult(t, []byte(`{"a":1,"b":["c","d"]}`), []byte(`{"a":1,"b":["c","e"]}`))) }) t.Run("inequality with array diff", func(t *testing.T) { assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Index: 1}}, Value1: `"b"`, Value2: `"c"`}, }, diffResult(t, []byte(`["a","b"]`), []byte(`["a","c"]`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Index: 1}, {Property: "b"}}, Value1: `2`, Value2: `3`}, }, diffResult(t, []byte(`["a",{"b":2}]`), []byte(`["a",{"b":3}]`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Index: 2}}, Value1: ``, Value2: `,"c"`}, }, diffResult(t, []byte(`["a","b"]`), []byte(`["a","b","c"]`))) assert.Equal(t, JSONDiffResult{ {Path: JSONPath{{Index: 2}}, Value1: `,"c"`, Value2: ``}, }, diffResult(t, []byte(`["a","b","c"]`), []byte(`["a","b"]`))) assert.Equal(t, JSONDiffResult{ {Path: nil, Value1: `["a","d"]`, Value2: `["a","b","c"]`}, }, diffResult(t, []byte(`["a","d"]`), []byte(`["a","b","c"]`))) }) } func TestJSONDiffResultStrings(t *testing.T) { assert.Equal(t, "x = abc, y = def", JSONDiffElement{Value1: "abc", Value2: "def"}.Describe("x", "y")) assert.Equal(t, `at "prop1": x = abc, y = def`, JSONDiffElement{Path: JSONPath{{Property: "prop1"}}, Value1: "abc", Value2: "def"}. Describe("x", "y")) assert.Equal(t, `at "prop1"."prop2": x = abc, y = def`, JSONDiffElement{Path: JSONPath{{Property: "prop1"}, {Property: "prop2"}}, Value1: "abc", Value2: "def"}. Describe("x", "y")) assert.Equal(t, "at [1]: x = abc, y = def", JSONDiffElement{Path: JSONPath{{Index: 1}}, Value1: "abc", Value2: "def"}. Describe("x", "y")) assert.Equal(t, `at [1]."prop2": x = abc, y = def`, JSONDiffElement{Path: JSONPath{{Index: 1}, {Property: "prop2"}}, Value1: "abc", Value2: "def"}. Describe("x", "y")) } go-test-helpers-3.0.2/jsonhelpers/json_formatting.go000066400000000000000000000026531430344127400226340ustar00rootroot00000000000000package jsonhelpers import ( "encoding/json" "sort" "strings" ) // ToJSON is just a shortcut for calling json.Marshal and taking only the first result. func ToJSON(value interface{}) []byte { ret, _ := json.Marshal(value) return ret } // ToJSONString calls json.Marshal and returns the result as a string. func ToJSONString(value interface{}) string { return string(ToJSON(value)) } // CanonicalizeJSON reformats a JSON value so that object properties are alphabetized, // making comparisons predictable and making it easier for a human reader to find a property. func CanonicalizeJSON(originalJSON []byte) []byte { var rootValue interface{} if err := json.Unmarshal(originalJSON, &rootValue); err != nil { return originalJSON } return []byte(toCanonicalizedString(rootValue)) } func toCanonicalizedString(value interface{}) string { switch v := value.(type) { case []interface{}: items := make([]string, 0, len(v)) for _, element := range v { items = append(items, toCanonicalizedString(element)) } return "[" + strings.Join(items, ",") + "]" case map[string]interface{}: keys := make([]string, 0, len(v)) for key := range v { keys = append(keys, key) } sort.Strings(keys) items := make([]string, 0, len(v)) for _, key := range keys { items = append(items, ToJSONString(key)+":"+toCanonicalizedString(v[key])) } return "{" + strings.Join(items, ",") + "}" default: return ToJSONString(v) } } go-test-helpers-3.0.2/jsonhelpers/json_formatting_test.go000066400000000000000000000015631430344127400236720ustar00rootroot00000000000000package jsonhelpers import ( "testing" "github.com/stretchr/testify/assert" ) func TestCanonicalizeJSON(t *testing.T) { assert.Equal(t, `null`, string(CanonicalizeJSON([]byte(`null`)))) assert.Equal(t, `true`, string(CanonicalizeJSON([]byte(`true`)))) assert.Equal(t, `123`, string(CanonicalizeJSON([]byte(`123`)))) assert.Equal(t, `"x"`, string(CanonicalizeJSON([]byte(`"x"`)))) assert.Equal(t, `{"a":1,"b":2,"c":3}`, string(CanonicalizeJSON([]byte(`{"a":1,"b":2,"c":3}`)))) assert.Equal(t, `{"a":1,"b":2,"c":3}`, string(CanonicalizeJSON([]byte(`{"c":3,"b":2,"a":1}`)))) assert.Equal(t, `{"a":1,"b":{"A":10,"B":20,"C":30},"c":3}`, string(CanonicalizeJSON([]byte(`{"c":3,"b":{"B":20,"A":10,"C":30},"a":1}`)))) assert.Equal(t, `[{"a":1,"b":2,"c":3},{"A":10,"B":20,"C":30}]`, string(CanonicalizeJSON([]byte(`[{"c":3,"b":2,"a":1},{"B":20,"A":10,"C":30}]`)))) } go-test-helpers-3.0.2/jsonhelpers/package_info.go000066400000000000000000000003451430344127400220330ustar00rootroot00000000000000// Package jsonhelpers contains general-purpose functions for manipulating JSON. // // These functions are not intended to be highly fast or efficient; they are designed for // simplicity of use in test code. package jsonhelpers go-test-helpers-3.0.2/jsonhelpers/value.go000066400000000000000000000035521430344127400205440ustar00rootroot00000000000000package jsonhelpers import ( "encoding/json" "fmt" "reflect" ) // JValue is a helper type for manipulating JSON data in tests. It validates that marshaled // data is valid JSON, allows other data to be converted to JSON, and eliminates ambiguity // as to whether a type like string or []byte in a test represents JSON or not. type JValue struct { raw string parsed any err error } // String returns the JSON value as a string. func (v JValue) String() string { return v.raw } // Error returns nil if the value is valid JSON, or else an error value describing the problem. func (v JValue) Error() error { return v.err } // Equal returns true if the values are deeply equal. func (v JValue) Equal(v1 JValue) bool { if v.err != nil || v1.err != nil { return v.err == v1.err } return reflect.DeepEqual(v.parsed, v1.parsed) } // JValueOf creates a JValue based on any input type, as follows: // // - If the input type is []byte, json.RawMessage, or string, it interprets the value as JSON. // - If the input type is JValue, it returns the same value. // - For any other type, it attempts to marshal the value to JSON. // // If the input value is invalid, the returned JValue will have a non-nil Error(). func JValueOf(value any) JValue { var data []byte switch v := value.(type) { case JValue: return v case json.RawMessage: data = v case []byte: data = v case string: data = []byte(v) default: d, err := json.Marshal(value) if err != nil { return JValue{ raw: "", parsed: value, err: fmt.Errorf("value could not be marshalled to JSON: %s", err), } } data = d } var intf interface{} if err := json.Unmarshal(data, &intf); err != nil { return JValue{ raw: string(data), parsed: nil, err: fmt.Errorf("JSON unmarshaling error: %s", err), } } return JValue{raw: string(data), parsed: intf, err: nil} } go-test-helpers-3.0.2/jsonhelpers/value_test.go000066400000000000000000000015541430344127400216030ustar00rootroot00000000000000package jsonhelpers import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestJValueOf(t *testing.T) { s := `{"a":true}` m := map[string]interface{}{"a": true} v1 := JValueOf([]byte(s)) assert.Nil(t, v1.Error()) assert.Equal(t, m, v1.parsed) assert.Equal(t, s, v1.String()) v2 := JValueOf(json.RawMessage(s)) assert.Nil(t, v2.Error()) assert.Equal(t, m, v2.parsed) assert.Equal(t, s, v2.String()) assert.Equal(t, v1, v2) v3 := JValueOf(s) assert.Nil(t, v3.Error()) assert.Equal(t, m, v3.parsed) assert.Equal(t, s, v3.String()) assert.Equal(t, v1, v3) v4 := JValueOf(m) assert.Nil(t, v4.Error()) assert.Equal(t, m, v4.parsed) assert.Equal(t, s, v4.String()) assert.Equal(t, v1, v4) v5 := JValueOf(v4) assert.Equal(t, v4, v5) v6 := JValueOf("{no") assert.NotNil(t, v6.Error()) assert.Equal(t, "{no", v6.String()) } go-test-helpers-3.0.2/matchers/000077500000000000000000000000001430344127400163465ustar00rootroot00000000000000go-test-helpers-3.0.2/matchers/api.go000066400000000000000000000063501430344127400174520ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // TestFunc is a function used in defining a new Matcher. It returns true if the value passes // the test or false for failure. type TestFunc func(value interface{}) bool // DescribeTestFunc is a function used in defining a new Matcher. It returns a description of // the test expectation. type DescribeTestFunc func() string // DescribeFailureFunc is a function used in defining a new Matcher. Given the value that was // tested, and assuming that the test failed, it returns a descriptive string. // // For simple conditions, this function can be omitted or can return an empty string, in which // case the failure description will be produced from only the DescribeTestFunc and a // description of the test value // // The second parameter is the function to use for making a string description of a value of // the expected type. type DescribeFailureFunc func(value interface{}) string // Matcher is a general mechanism for declaring expectations about a value. Expectations can be combined, // and they self-describe on failure. type Matcher struct { testFn TestFunc describeTestFn DescribeTestFunc describeFailureFn DescribeFailureFunc } // New creates a Matcher. func New( test TestFunc, describeTest DescribeTestFunc, describeFailure DescribeFailureFunc, ) Matcher { return Matcher{testFn: test, describeTestFn: describeTest, describeFailureFn: describeFailure} } // Test executes the expectation for a specific value. It returns true if the value passes the // test or false for failure, plus a string describing the expectation that failed. func (m Matcher) Test(value interface{}) (pass bool, failDescription string) { if m.test(value) { return true, "" } var failureDesc string if m.describeFailureFn != nil { failureDesc = m.describeFailureFn(value) } if failureDesc == "" { failureDesc = fmt.Sprintf("expected: %s", m.describeTest()) } return false, fmt.Sprintf("%s\nfull value was: %s", failureDesc, DescribeValue(value)) } func (m Matcher) test(value interface{}) bool { if m.testFn == nil { return true } return m.testFn(value) } func (m Matcher) describeTest() string { if m.describeTestFn == nil { return "[no description given for assertion]" } return m.describeTestFn() } func (m Matcher) describeFailure(value interface{}) string { if m.describeFailureFn != nil { return m.describeFailureFn(value) } return m.describeTest() } // EnsureType adds type safety to a matcher. The valueOfType parameter should be any value of the // expected type. The returned Matcher will guarantee that the value is of that type before calling // the original test function, so it is safe for the test function to cast the value. func (m Matcher) EnsureType(valueOfType interface{}) Matcher { return New( func(value interface{}) bool { if valueOfType != nil && (reflect.TypeOf(value) != reflect.TypeOf(valueOfType)) { return false } return m.test(value) }, m.describeTest, func(value interface{}) string { if valueOfType != nil && reflect.TypeOf(value) != reflect.TypeOf(valueOfType) { return fmt.Sprintf("expected value of type %T, was %T", valueOfType, value) } if m.describeFailureFn == nil { return "" } return m.describeFailure(value) }, ) } go-test-helpers-3.0.2/matchers/api_test.go000066400000000000000000000046041430344127400205110ustar00rootroot00000000000000package matchers import ( "fmt" "regexp" "strings" "testing" "github.com/stretchr/testify/assert" ) type decoratedString string func (s decoratedString) String() string { return decorate(string(s)) } func decorate(value interface{}) string { return fmt.Sprintf("Hi, I'm '%s'", value.(string)) } func assertPasses(t *testing.T, value interface{}, m Matcher) { pass, desc := m.Test(value) assert.True(t, pass) assert.Equal(t, "", desc) } func assertFails(t *testing.T, value interface{}, m Matcher, expectedDesc string) { pass, desc := m.Test(value) assert.False(t, pass) if strings.Contains(expectedDesc, "full value was:") { assert.Equal(t, expectedDesc, desc) } else { assert.Regexp(t, regexp.MustCompile("^"+regexp.QuoteMeta(expectedDesc)), desc) } } func TestUninitializedMatcher(t *testing.T) { m := Matcher{} assertPasses(t, "whatever", m) } func TestSimpleMatcher(t *testing.T) { m := New( func(value interface{}) bool { return value == "good" }, func() string { return "should be good" }, nil, ) assertPasses(t, "good", m) assertFails(t, "bad", m, `expected: should be good`+"\n"+`full value was: "bad"`) } func TestSimpleMatcherWithFailureDescription(t *testing.T) { m := New( func(value interface{}) bool { return value == "good" }, func() string { return "should be good" }, func(interface{}) string { return "was not good" }, ) assertPasses(t, "good", m) assertFails(t, "bad", m, `was not good`+"\n"+`full value was: "bad"`) } func TestEnsureType(t *testing.T) { m := New( func(value interface{}) bool { return value == "good" }, func() string { return "should be good" }, nil, ) assertPasses(t, "good", m) assertFails(t, 3, m, "expected: should be good\nfull value was: 3") m1 := m.EnsureType("example string") assertPasses(t, "good", m1) assertFails(t, "bad", m1, `expected: should be good`+"\n"+`full value was: "bad"`) assertFails(t, 3, m1, "expected value of type string, was int\nfull value was: 3") m2 := m.EnsureType(nil) // no-op assertPasses(t, "good", m2) assertFails(t, 3, m2, "expected: should be good\nfull value was: 3") } func TestMatcherUsesDescribeValue(t *testing.T) { m := New( func(value interface{}) bool { return value == decoratedString("good") }, func() string { return "should be good" }, nil, ) assertFails(t, decoratedString("bad"), m, fmt.Sprintf("expected: should be good\nfull value was: %s", decorate("bad"))) } go-test-helpers-3.0.2/matchers/assertion_scope.go000066400000000000000000000062351430344127400221030ustar00rootroot00000000000000package matchers // TestingT is an interface for any test scope type that has an Errorf method for reporting // failures, and a FailNow method for stopping the test immediately. This is compatible with // Go's testing.T, and with assert.TestingT and require.TestingT. See Test and For. type TestingT interface { Errorf(format string, args ...interface{}) FailNow() } // Go's testing.T and other compatible packages provide an additional method, Helper(), // which indicates that whatever function called it should not be included in stacktraces. type helperT interface { Helper() } // AssertionScope is a context for executing assertions. type AssertionScope struct { t TestingT prefix string } // In is for use with any test framework that has a test scope type with the same basic methods // as Go's testing.T (as defined by the TestingT interface). Any calls to Assert or Require on // the returned AssertionScope will update the state of t. // // func TestSomething(t *testing.T) { // matchers.In(t).Assert(x, matchers.Equal(2)) // } // // See also For. func In(t TestingT) AssertionScope { return AssertionScope{t: t} } // For is the same as In, but adds a descriptive name in front of whatever assertions are done. // In this example, a failure would be logged as "score: does not equal 2" rather than only // "does not equal 2". // // func TestSomething(t *testing.T) { // matchers.For(t, "score").Assert(x, matchers.Equal(2)) // } func For(t TestingT, name string) AssertionScope { return AssertionScope{t: t, prefix: name + ": "} } // For returns a new AssertionScope that has an additional name prefix. In this example, // a failure would be logged as "final: score: does not equal 2" rather than only // "does not equal 2". // // func TestSomething(t *testing.T) { // matchers.In(t).For("final").For("score").Assert(x, matchers.Equal(2)) // } func (a AssertionScope) For(name string) AssertionScope { return AssertionScope{t: a.t, prefix: a.prefix + name + ": "} } // Assert is for use with any test framework that has a test scope type with the same Errorf // method as Go's testing.T. It tests a value against a matcher and, on failure, calls the test // scope's Errorf method. This logs a failure but does not stop the test. func (a AssertionScope) Assert(value interface{}, matcher Matcher) bool { if pass, desc := matcher.Test(value); !pass { if h, ok := a.t.(helperT); ok { h.Helper() } a.fail(desc) return false } return true } // Require is for use with any test framework that has a test scope type with the same Errorf // and FailNow methods as Go's testing.T. It tests a value against a matcher and, on failure, calls // the test scope's Errorf method and then FailNow. This logs a failure and immediately terminates // the test. func (a AssertionScope) Require(value interface{}, matcher Matcher) bool { if pass, desc := matcher.Test(value); !pass { if h, ok := a.t.(helperT); ok { h.Helper() } a.fail(desc) a.t.FailNow() return false // does not return since FailNow() will force an early exit } return true } func (a AssertionScope) fail(desc string) { if h, ok := a.t.(helperT); ok { h.Helper() } a.t.Errorf("%s%s", a.prefix, desc) } go-test-helpers-3.0.2/matchers/assertion_scope_test.go000066400000000000000000000042411430344127400231350ustar00rootroot00000000000000package matchers import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type fakeTestScope struct { failures []string terminated bool } func (t *fakeTestScope) Errorf(format string, args ...interface{}) { t.failures = append(t.failures, fmt.Sprintf(format, args...)) } func (t *fakeTestScope) FailNow() { t.terminated = true } func TestAssertionScopeFor(t *testing.T) { test1 := fakeTestScope{} In(&test1).For("a").Assert(2, Equal(3)) require.Len(t, test1.failures, 1) assert.Regexp(t, "^a: did not equal 3", test1.failures[0]) test2 := fakeTestScope{} For(&test2, "a").Assert(2, Equal(3)) require.Len(t, test2.failures, 1) assert.Regexp(t, "^a: did not equal 3", test2.failures[0]) test3 := fakeTestScope{} In(&test3).For("a").For("b").Assert(2, Equal(3)) require.Len(t, test3.failures, 1) assert.Regexp(t, "^a: b: did not equal 3", test3.failures[0]) } func TestAssert(t *testing.T) { test1 := fakeTestScope{} In(&test1).Assert(2, Equal(2)) assert.Len(t, test1.failures, 0) assert.False(t, test1.terminated) test2 := fakeTestScope{} In(&test2).Assert(3, Equal(2)) In(&test2).Assert(4, Equal(2)) require.Len(t, test2.failures, 2) assert.False(t, test2.terminated) assert.Equal(t, "did not equal 2\nfull value was: 3", test2.failures[0]) assert.Equal(t, "did not equal 2\nfull value was: 4", test2.failures[1]) test3 := fakeTestScope{} For(&test3, "score").Assert(3, Equal(2)) require.Len(t, test3.failures, 1) assert.False(t, test3.terminated) assert.Equal(t, "score: did not equal 2\nfull value was: 3", test3.failures[0]) } func TestRequire(t *testing.T) { test1 := fakeTestScope{} In(&test1).Require(2, Equal(2)) assert.Len(t, test1.failures, 0) assert.False(t, test1.terminated) test2 := fakeTestScope{} In(&test2).Require(3, Equal(2)) assert.Len(t, test2.failures, 1) assert.True(t, test2.terminated) assert.Equal(t, "did not equal 2\nfull value was: 3", test2.failures[0]) test3 := fakeTestScope{} For(&test3, "score").Require(3, Equal(2)) require.Len(t, test3.failures, 1) assert.True(t, test3.terminated) assert.Equal(t, "score: did not equal 2\nfull value was: 3", test3.failures[0]) } go-test-helpers-3.0.2/matchers/basic_matchers.go000066400000000000000000000030501430344127400216420ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // Equal is a matcher that tests whether the input value matches the expected value according // to reflect.DeepEqual, except in the case of numbers where an exact type match is not needed. func Equal(expectedValue interface{}) Matcher { return New( func(value interface{}) bool { return reflect.DeepEqual(canonicalizeValue(value), canonicalizeValue(expectedValue)) }, func() string { return fmt.Sprintf("equal to %s", DescribeValue(expectedValue)) }, func(value interface{}) string { return fmt.Sprintf("did not equal %s", DescribeValue(expectedValue)) }, ) } // BeNil is a matcher that passes if the value is a nil interface value, a nil pointer, a // nil slice, or a nil map. func BeNil() Matcher { return New( func(value interface{}) bool { if value == nil { return true } rv := reflect.ValueOf(value) switch rv.Type().Kind() { case reflect.Ptr, reflect.Slice, reflect.Map: return rv.IsNil() } return false }, func() string { return "is nil" }, func(value interface{}) string { return "was not nil" }, ) } // Automatic numeric type conversion for use with Equals(), to avoid the common problem of // expecting for instance int(1) in a JSON data structure which parsed it as float64(1). func canonicalizeValue(value interface{}) interface{} { switch v := value.(type) { case uint8: return uint64(v) case uint: return uint64(v) case int8: return float64(v) case int: return float64(v) case float32: return float64(v) } return value } go-test-helpers-3.0.2/matchers/basic_matchers_test.go000066400000000000000000000005441430344127400227060ustar00rootroot00000000000000package matchers import "testing" func TestEqual(t *testing.T) { assertPasses(t, 3, Equal(3)) assertFails(t, 4, Equal(3), "did not equal 3\nfull value was: 4") assertPasses(t, 3, Equal(float64(3))) assertPasses(t, float64(3), Equal(3)) assertPasses(t, map[string]interface{}{"a": []int{1, 2}}, Equal(map[string]interface{}{"a": []int{1, 2}})) } go-test-helpers-3.0.2/matchers/collections.go000066400000000000000000000265521430344127400212250ustar00rootroot00000000000000package matchers import ( "errors" "fmt" "reflect" "sort" "strings" ) // KeyValueMatcher is used with MapOf or MapIncluding to describe a matcher for a key-value pair in a map. type KeyValueMatcher struct { Key interface{} Value Matcher } // KV is a shortcut for constructing a KeyValueMatcher for use with MapOf or MapIncluding. func KV(key interface{}, valueMatcher Matcher) KeyValueMatcher { return KeyValueMatcher{Key: key, Value: valueMatcher} } // Items is a matcher for a slice or array value. It tests that the number of elements is equal to // the number of matchers, and that each element matches the corresponding matcher in order. // // s := []int{6,2} // matchers.Items(matchers.Equal(6), matchers.Equal(2)).Test(s) // pass // matchers.Items(matchers.Equal(2), matchers.Equal(6)).Test(s) // fail func Items(matchers ...Matcher) Matcher { return New( func(value interface{}) bool { elements, err := getSliceOrArrayElementValues(value) if err != nil || len(elements) != len(matchers) { return false } for i, m := range matchers { elementValue := elements[i] if !m.test(elementValue) { return false } } return true }, func() string { return "items: " + describeMatchers(matchers, "") }, func(value interface{}) string { elements, err := getSliceOrArrayElementValues(value) if err != nil { return err.Error() } if len(elements) != len(matchers) { return fmt.Sprintf("expected slice with %d item(s), got %d item(s)", len(matchers), len(elements)) } parts := make([]string, 0, len(matchers)) for i, m := range matchers { elementValue := elements[i] if !m.test(elementValue) { parts = append(parts, fmt.Sprintf("item[%d] %s", i, m.describeFailure(elementValue))) } } return strings.Join(parts, ", ") }, ) } // ItemsInAnyOrder is a matcher for a slice or array value. It tests that the number of elements is // equal to the number of matchers, and that each matcher matches an element. // // s := []int{6,2} // matchers.ItemsInAnyOrder(matchers.Equal(2), matchers.Equal(6)).Test(s) // pass func ItemsInAnyOrder(matchers ...Matcher) Matcher { return New( func(value interface{}) bool { elements, err := getSliceOrArrayElementValues(value) if err != nil || len(elements) != len(matchers) { return false } foundCount := 0 for _, m := range matchers { for _, elementValue := range elements { if m.test(elementValue) { foundCount++ break } } } return foundCount == len(matchers) }, func() string { return "items in any order: " + describeMatchers(matchers, ", ") }, func(value interface{}) string { // Describing a failure for ItemsInAnyOrder requires us to repeat the matching logic we // previously executed, but in a bit more detail. For any matcher that successfully // matched an item, we don't need to describe that matcher or that item. elements, err := getSliceOrArrayElementValues(value) if err != nil { return err.Error() } type unmatchedElement struct { index int value interface{} } unmatchedElements := make([]unmatchedElement, 0) for i, e := range elements { unmatchedElements = append(unmatchedElements, unmatchedElement{index: i, value: e}) } unmatchedMatchers := append([]Matcher(nil), matchers...) for i := 0; i < len(unmatchedMatchers); i++ { m := unmatchedMatchers[i] for j := 0; j < len(unmatchedElements); j++ { if m.test(unmatchedElements[j].value) { unmatchedElements = append(unmatchedElements[0:j], unmatchedElements[j+1:]...) unmatchedMatchers = append(unmatchedMatchers[0:i], unmatchedMatchers[i+1:]...) j-- i-- break } } } if len(unmatchedMatchers) == 0 { // Every matcher matched a value but there were some values left over. parts := make([]string, 0) for _, e := range unmatchedElements { parts = append(parts, fmt.Sprintf("[%d]: %s", e.index, DescribeValue(e.value))) } return fmt.Sprintf("got more items than expected: %s", strings.Join(parts, ", ")) } if len(unmatchedMatchers) == 1 && len(unmatchedElements) == 1 { // In this case we'll assume that this was the element that was supposed to be matched // by this matcher, but its value was wrong somehow. So we'll print the failure message // from the matcher. return fmt.Sprintf("failed expectation for one item [%d] with value: %s\nfailure was: %s", unmatchedElements[0].index, DescribeValue(unmatchedElements[0].value), unmatchedMatchers[0].describeFailure(unmatchedElements[0].value)) } // If there was more than one unmatched matcher and/or element, we can't really guess // which one was supposed to go with which, so we'll just print all the unmet conditions. return fmt.Sprintf("no items were found to match: %s", describeMatchers(unmatchedMatchers, ", ")) }, ) } // MapOf is a matcher for a map value. It tests that the map has exactly the same keys as the // specified list, and that the matcher for each key is satisfied by the corresponding value. // // m := map[string]int{"a": 6, "b": 2} // matchers.MapOf( // matchers.KV("a", matchers.Equal(2)), // matchers.KV("b", matchers.Equal(6)), // }).Test(s) // pass func MapOf(keyValueMatchers ...KeyValueMatcher) Matcher { return New( func(value interface{}) bool { valueAsMap, err := getMapValues(value) if err != nil || len(valueAsMap) != len(keyValueMatchers) { return false } for _, kv := range keyValueMatchers { if elementValue, ok := valueAsMap[kv.Key]; ok { if !kv.Value.test(elementValue) { return false } } else { return false } } return true }, func() string { var parts []string for _, kv := range keyValueMatchers { parts = append(parts, fmt.Sprintf("%s: %s", kv.Key, kv.Value.describeTest())) } return "map: {" + strings.Join(parts, ", ") + "}" }, func(value interface{}) string { valueAsMap, err := getMapValues(value) if err != nil { return err.Error() } if len(valueAsMap) != len(keyValueMatchers) { return fmt.Sprintf("expected map keys %v but got map keys %v", getSortedExpectedKeys(keyValueMatchers), getSortedMapKeys(valueAsMap)) } parts := make([]string, 0, len(keyValueMatchers)) for _, kv := range keyValueMatchers { if elementValue, ok := valueAsMap[kv.Key]; ok { if !kv.Value.test(elementValue) { parts = append(parts, fmt.Sprintf("key [%s] %s", kv.Key, kv.Value.describeFailure(elementValue))) } } else { parts = append(parts, fmt.Sprintf("key [%s] not found", kv.Key)) } } return strings.Join(parts, ", ") }, ) } // MapIncluding is a matcher for a map value. It tests that the map contains all of the keys in // the specified list, and that the matcher for each key is satisfied by the corresponding value. // The map may also contain additional keys. // // m := map[string]int{"a": 6, "b": 2} // matchers.MapOf( // matchers.KV("a", matchers.Equal(2)), // matchers.KV("b", matchers.Equal(6)), // }).Test(s) // pass func MapIncluding(keyValueMatchers ...KeyValueMatcher) Matcher { return New( func(value interface{}) bool { valueAsMap, err := getMapValues(value) if err != nil { return false } for _, kv := range keyValueMatchers { if elementValue, ok := valueAsMap[kv.Key]; ok { if !kv.Value.test(elementValue) { return false } } else { return false } } return true }, func() string { var parts []string for _, kv := range keyValueMatchers { parts = append(parts, fmt.Sprintf("%s: %s", kv.Key, kv.Value.describeTest())) } return "map including: {" + strings.Join(parts, ", ") + "}" }, func(value interface{}) string { valueAsMap, err := getMapValues(value) if err != nil { return err.Error() } parts := make([]string, 0, len(keyValueMatchers)) for _, kv := range keyValueMatchers { if elementValue, ok := valueAsMap[kv.Key]; ok { if !kv.Value.test(elementValue) { parts = append(parts, fmt.Sprintf("key [%s] %s", kv.Key, kv.Value.describeFailure(elementValue))) } } else { parts = append(parts, fmt.Sprintf("key [%s] not found", kv.Key)) } } return strings.Join(parts, ", ") }, ) } func getSliceOrArrayElementValues(sliceValue interface{}) ([]interface{}, error) { v := reflect.ValueOf(sliceValue) if v.Type().Kind() != reflect.Slice && v.Type().Kind() != reflect.Array { return nil, fmt.Errorf("expected slice or array value but got %T", sliceValue) } ret := make([]interface{}, 0, v.Len()) for i := 0; i < v.Len(); i++ { ret = append(ret, v.Index(i).Interface()) } return ret, nil } func getMapValues(mapValue interface{}) (map[interface{}]interface{}, error) { v := reflect.ValueOf(mapValue) if v.Type().Kind() != reflect.Map { return nil, fmt.Errorf("expected map value but got %T", mapValue) } ret := make(map[interface{}]interface{}, v.Len()) for _, k := range v.MapKeys() { ret[k.Interface()] = v.MapIndex(k).Interface() } return ret, nil } func getSortedExpectedKeys(keyValueMatchers []KeyValueMatcher) []string { ret := make([]string, 0, len(keyValueMatchers)) for _, kv := range keyValueMatchers { ret = append(ret, fmt.Sprintf("%v", kv.Key)) } sort.Strings(ret) return ret } func getSortedMapKeys(mapValue interface{}) []string { v := reflect.ValueOf(mapValue) if v.Type().Kind() != reflect.Map { return nil } ret := make([]string, 0, v.Len()) for _, k := range v.MapKeys() { ret = append(ret, fmt.Sprintf("%v", k.Interface())) } sort.Strings(ret) return ret } // ValueForKey is a MatcherTransform that takes a map, looks up a value in it by key, // and applies a matcher to that value. It fails if no such key exists (see // OptValueForKey). // // myMap := map[string]map[string]int{"a": map[string]int{"b": 2}} // matchers.In(t).Assert(myObject, // matchers.ValueForKey("a").Should( // matchers.ValueForKey("b").Should(Equal(2)))) func ValueForKey(key interface{}) MatcherTransform { return Transform( fmt.Sprintf("for key %s", DescribeValue(key)), func(value interface{}) (interface{}, error) { if value == nil { return nil, errors.New("map was nil") } rv := reflect.ValueOf(value) if rv.Type().Kind() != reflect.Map { return nil, fmt.Errorf("expected a map but got %T", value) } for _, k := range rv.MapKeys() { if k.Interface() == key { return rv.MapIndex(k).Interface(), nil } } return nil, fmt.Errorf("map key %s not found", DescribeValue(key)) }, ) } // OptValueForKey is a MatcherTransform that takes a map, looks up a value in it by key, // and applies a matcher to that value. If no such key exists, it returns the zero // value for the type. If the map was nil, it returns nil. // // myMap := map[string]map[string]int{"a": map[string]int{"b": 2}} // matchers.In(t).Assert(myMap, // matchers.OptValueForKey("a").Should( // matchers.OptValueForKey("c").Should(Equal(0)))) func OptValueForKey(key interface{}) MatcherTransform { return Transform( fmt.Sprintf("for key %s", DescribeValue(key)), func(value interface{}) (interface{}, error) { if value == nil { return nil, nil } rv := reflect.ValueOf(value) if rv.Type().Kind() != reflect.Map { return nil, fmt.Errorf("expected a map but got %T", value) } result := rv.MapIndex(reflect.ValueOf(key)) if !result.IsValid() { return reflect.Zero(rv.Type().Elem()).Interface(), nil } return result.Interface(), nil }, ) } go-test-helpers-3.0.2/matchers/collections_test.go000066400000000000000000000062641430344127400222620ustar00rootroot00000000000000package matchers import "testing" func TestItems(t *testing.T) { slice := []string{"y", "z", "x"} array := [3]string{"y", "z", "x"} assertPasses(t, slice, Items(Equal("y"), Equal("z"), Equal("x"))) assertPasses(t, array, Items(Equal("y"), Equal("z"), Equal("x"))) assertFails(t, slice, Items(Equal("a"), Equal("b"), Equal("c")), `item[0] did not equal "a", item[1] did not equal "b", item[2] did not equal "c"`) assertFails(t, slice, Items(Equal("y"), Equal("b"), Equal("x")), `item[1] did not equal "b"`) assertFails(t, slice, Items(Equal("x"), Equal("y")), "expected slice with 2 item(s), got 3 item(s)") assertFails(t, 2, Items(Equal("x"), Equal("y")), "expected slice or array value but got int\nfull value was: 2") } func TestItemsInAnyOrder(t *testing.T) { slice := []string{"y", "z", "x"} array := [3]string{"y", "z", "x"} assertPasses(t, slice, ItemsInAnyOrder(Equal("x"), Equal("y"), Equal("x"))) assertPasses(t, slice, ItemsInAnyOrder(Equal("y"), Equal("z"), Equal("x"))) assertPasses(t, array, ItemsInAnyOrder(Equal("x"), Equal("y"), Equal("x"))) assertFails(t, slice, ItemsInAnyOrder(Equal("x"), Equal("y")), `got more items than expected: [1]: "z"`) assertFails(t, slice, ItemsInAnyOrder(Equal("x"), Equal("a"), Equal("z")), `failed expectation for one item [0] with value: "y"`+"\n"+ `failure was: did not equal "a"`) assertFails(t, slice, ItemsInAnyOrder(Equal("x"), Equal("a"), Equal("b")), `no items were found to match: (equal to "a"), (equal to "b")`) assertFails(t, slice, ItemsInAnyOrder(Equal("a"), Equal("b"), Equal("c")), `no items were found to match: (equal to "a"), (equal to "b"), (equal to "c")`) } func TestMapOf(t *testing.T) { m := map[string]int{"a": 1, "b": 2} assertPasses(t, m, MapOf( KV("b", Equal(2)), KV("a", Equal(1)), )) assertFails(t, m, MapOf( KV("b", Equal(3)), KV("a", Equal(1)), ), `key [b] did not equal 3`) assertFails(t, m, MapOf( KV("b", Equal(2)), ), `expected map keys [b] but got map keys [a b]`) } func TestMapIncluding(t *testing.T) { m := map[string]int{"a": 1, "b": 2} assertPasses(t, m, MapIncluding( KV("b", Equal(2)), KV("a", Equal(1)), )) assertPasses(t, m, MapIncluding( KV("b", Equal(2)), )) assertFails(t, m, MapIncluding( KV("b", Equal(3)), KV("a", Equal(1)), ), `key [b] did not equal 3`) assertFails(t, m, MapIncluding( KV("c", Equal(3)), KV("b", Equal(2)), KV("a", Equal(1)), ), `key [c] not found`) } func TestValueForKey(t *testing.T) { m := map[string]int{"a": 1, "b": 2} assertPasses(t, m, ValueForKey("b").Should(Equal(2))) assertFails(t, m, ValueForKey("c").Should(Equal(2)), `map key "c" not found`) assertFails(t, []int{}, ValueForKey("c").Should(Equal(2)), `expected a map but got []int`) assertFails(t, nil, ValueForKey("c").Should(Equal(2)), `map was nil`) } func TestOptValueForKey(t *testing.T) { m1 := map[string]int{"a": 1, "b": 2} m2 := map[string]interface{}{"a": 1, "b": 2} assertPasses(t, m1, OptValueForKey("b").Should(Equal(2))) assertPasses(t, m1, OptValueForKey("c").Should(Equal(0))) assertFails(t, []int{}, OptValueForKey("c").Should(Equal(2)), `expected a map but got []int`) assertPasses(t, m2, OptValueForKey("c").Should(BeNil())) } go-test-helpers-3.0.2/matchers/combinators.go000066400000000000000000000027411430344127400212210ustar00rootroot00000000000000package matchers import ( "fmt" ) // Not negates the result of another Matcher. // // matchers.Not(Equal(3)).Assert(t, 4) // // failure message will describe expectation as "not (equal to 3)" func Not(matcher Matcher) Matcher { return New( func(value interface{}) bool { return !matcher.test(value) }, func() string { return fmt.Sprintf("not (%s)", matcher.describeTest()) }, nil, ) } // AllOf requires that the input value passes all of the specified Matchers. If it fails, // the failure message describes all of the Matchers that failed. func AllOf(matchers ...Matcher) Matcher { return New( func(value interface{}) bool { for _, m := range matchers { if !m.test(value) { return false } } return true }, func() string { return describeMatchers(matchers, " and ") }, func(value interface{}) string { return describeFailures(matchers, value) }, ) } // AnyOf requires that the input value passes at least one of the specified Matchers. It will // not execute any further matches after the first pass. If it fails all of them, the failure // message describes all of the failure conditions. func AnyOf(matchers ...Matcher) Matcher { return New( func(value interface{}) bool { for _, m := range matchers { if m.test(value) { return true } } return false }, func() string { return describeMatchers(matchers, " and ") }, func(value interface{}) string { return describeFailures(matchers, value) }, ) } go-test-helpers-3.0.2/matchers/combinators_test.go000066400000000000000000000017741430344127400222650ustar00rootroot00000000000000package matchers import ( "testing" ) func TestNot(t *testing.T) { assertPasses(t, "bad", Not(Equal("good"))) assertFails(t, "good", Not(Equal("good")), `expected: not (equal to "good")`+"\n"+`full value was: "good"`) } func TestAllOf(t *testing.T) { hasA := StringContains("A") hasB := StringContains("B") assertPasses(t, "an A and a B", AllOf(hasA, hasB)) assertFails(t, "a B", AllOf(hasA, hasB), `did not contain "A"`+"\n"+`full value was: "a B"`) assertFails(t, "an A", AllOf(hasA, hasB), `did not contain "B"`+"\n"+`full value was: "an A"`) assertFails(t, "a C", AllOf(hasA, hasB), `did not contain "A", did not contain "B"`+"\n"+`full value was: "a C"`) } func TestAnyOf(t *testing.T) { hasA := StringContains("A") hasB := StringContains("B") assertPasses(t, "an A and a B", AnyOf(hasA, hasB)) assertPasses(t, "a B", AnyOf(hasA, hasB)) assertPasses(t, "an A", AnyOf(hasA, hasB)) assertFails(t, "a C", AnyOf(hasA, hasB), `did not contain "A", did not contain "B"`+"\n"+`full value was: "a C"`) } go-test-helpers-3.0.2/matchers/json.go000066400000000000000000000145521430344127400176550ustar00rootroot00000000000000package matchers import ( "encoding/json" "errors" "fmt" "reflect" "strings" "github.com/launchdarkly/go-test-helpers/v3/jsonhelpers" ) // JSONEqual is similar to Equal but with richer behavior for JSON values. // // Both the expected value and the actual value can be of any type. If the type is either []byte // or json.RawMessage, it will be interpreted as JSON which will be parsed; for all other types, // it will be first serialized to JSON with json.Marshal and then parsed. Then the parsed values // or data structures are tested for deep equality. For instance, this test passes: // // matchers.In(t).Assert([]byte(`{"a": true, "b": false`), // matchers.JSONEqual(map[string]bool{b: false, a: true})) // // The shortcut JSONEqualStr can be used to avoid writing []byte() if the expected value is // already a serialized JSON string. func JSONEqual(expectedValue interface{}) Matcher { expectedIntf, expectedValueErr := toJSONInterface(expectedValue) return New( func(value interface{}) bool { if expectedValueErr != nil { return false } valueIntf, err := toJSONInterface(value) if err != nil { return false } return reflect.DeepEqual(valueIntf, expectedIntf) }, func() string { return fmt.Sprintf("JSON equal to %s", jsonhelpers.CanonicalizeJSON(jsonhelpers.ToJSON(expectedIntf))) }, func(value interface{}) string { if expectedValueErr != nil { return fmt.Sprintf("bad expected value in assertion (%s)", expectedValueErr) } valueIntf, err := toJSONInterface(value) if err != nil { return err.Error() } diff, err := jsonhelpers.JSONDiff(jsonhelpers.ToJSON(expectedIntf), jsonhelpers.ToJSON(valueIntf)) if err != nil { return err.Error() } if len(diff) == 1 && diff[0].Path == nil { return fmt.Sprintf("expected: JSON equal to %s", diff[0].Value1) } return "JSON values " + strings.Join(diff.Describe("expected", "actual"), "\n") }, ) } // JSONStrEqual is equivalent to JSONEqual except that it converts expectedValue from string // to []byte first, and if the input value is a string it does the same. This is convenient if // you are matching against already-serialized JSON, because otherwise passing a string value // to JSONEqual would cause that value to be serialized in the way JSON represents strings, // that is, with quoting and escaping. // // matchers.In(t).Assert(`{"a": true, "b": false`, // matchers.JSONStrEqual(`{"b": false, "a": true}`) func JSONStrEqual(expectedValue string) Matcher { return Transform("", func(value interface{}) (interface{}, error) { if s, ok := value.(string); ok { return []byte(s), nil } return value, nil }).Should(JSONEqual([]byte(expectedValue))) } // JSONProperty is a MatcherTransform that takes any value serializable as a JSON object // and gets a named property from it; then you can apply a matcher to the value of that // property. It fails if no such property exists (see OptJSONProperty). // // myObject := []byte(`{"a": {"b": 2}}`) // matchers.In(t).Assert(myObject, // matchers.JSONProperty("a").Should( // matchers.JSONProperty("b").Should(Equal(2)))) // // An alternative is to use JSONMap combined with MapOf or MapIncluding. func JSONProperty(name string) MatcherTransform { return Transform( fmt.Sprintf("JSON property %q", name), func(value interface{}) (interface{}, error) { m, err := toJSONObjectMap(value) if err != nil { return nil, err } if propValue, ok := m[name]; ok { return propValue, nil } return nil, fmt.Errorf("JSON property %q not found", name) }, ) } // JSONOptProperty is the same as JSONProperty, but if the property does not exist, it treats it // as a nil value rather than error. func JSONOptProperty(name string) MatcherTransform { return Transform( fmt.Sprintf("JSON property %q", name), func(value interface{}) (interface{}, error) { m, err := toJSONObjectMap(value) if err != nil { return nil, err } return m[name], nil }, ) } // JSONArray is a MatcherTransform that takes any value serializable as a JSON array, and converts // it to []interface{} slice; then you can apply a matcher to that slice. It fails if the value is // not serializable as a JSON array. // // myArray := []byte(`["a", "b", "c"]`) // matchers.In(t).Assert(myArray, // matchers.JSONArray().Should(matchers.Length().Should(matchers.Equal(3)))) func JSONArray() MatcherTransform { return Transform( "JSON array", func(value interface{}) (interface{}, error) { v, err := toJSONInterface(value) if err != nil { return nil, err } if s, ok := v.([]interface{}); ok { return s, nil } return nil, errors.New("wanted a JSON array but found a different type") }, ) } // JSONMap is a MatcherTransform that takes any value serializable as a JSON object, and converts // it to a map[interface{}]interface{}; then you can apply a matcher to that map. It fails if the // value is not serializable as a JSON object. // // myArray := []byte(`{"a": 1, "b": "xyz"}`) // matchers.In(t).Assert(myJSON, // matchers.JSONMap().Should( // matchers.MapOf( // matchers.KV("a", matchers.Equal(1)), // matchers.KV("b", matchers.StringHasPrefix("x")), // ))) func JSONMap() MatcherTransform { return Transform( "JSON map", func(value interface{}) (interface{}, error) { m, err := toJSONObjectMap(value) if err != nil { return nil, err } return m, nil }, ) } func toJSONInterface(value interface{}) (interface{}, error) { var data []byte switch v := value.(type) { case json.RawMessage: data = v case []byte: data = v default: d, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("value could not be marshalled to JSON: %s", err) } data = d } var intf interface{} if err := json.Unmarshal(data, &intf); err != nil { return nil, fmt.Errorf("value was not valid JSON: %s", err) } return intf, nil } func toJSONObjectMap(value interface{}) (map[string]interface{}, error) { valueIntf, err := toJSONInterface(value) if err != nil { return nil, err } if m, ok := valueIntf.(map[string]interface{}); ok { return m, nil } if s, ok := valueIntf.(string); ok { if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") { var m map[string]interface{} if err := json.Unmarshal([]byte(s), &m); err == nil { return m, nil } } } return nil, errors.New("wanted a JSON object but found a different type") } go-test-helpers-3.0.2/matchers/json_test.go000066400000000000000000000063611430344127400207130ustar00rootroot00000000000000package matchers import ( "encoding/json" "testing" ) func TestJSONEqual(t *testing.T) { t.Run("simple values", func(t *testing.T) { for _, value := range []interface{}{nil, true, false, 3, 3.5, "x"} { jsonValue, _ := json.Marshal(value) t.Run(string(jsonValue), func(t *testing.T) { assertPasses(t, value, JSONEqual(value)) assertPasses(t, jsonValue, JSONEqual(value)) assertPasses(t, value, JSONEqual(jsonValue)) assertPasses(t, string(jsonValue), JSONStrEqual(string(jsonValue))) }) } }) t.Run("deep equality", func(t *testing.T) { for _, value := range []interface{}{ []string{"a", "b"}, map[string]interface{}{"a": []int{1, 2}}, } { jsonValue, _ := json.Marshal(value) t.Run(string(jsonValue), func(t *testing.T) { assertPasses(t, value, JSONEqual(value)) assertPasses(t, jsonValue, JSONEqual(value)) assertPasses(t, value, JSONEqual(jsonValue)) assertPasses(t, value, JSONStrEqual(string(jsonValue))) assertPasses(t, string(jsonValue), JSONStrEqual(string(jsonValue))) }) } assertPasses(t, []byte(`{"a": true, "b": false}`), JSONEqual([]byte(`{"b": false, "a": true}`))) }) t.Run("inequality with basic message", func(t *testing.T) { assertFails(t, true, JSONEqual(3), "expected: JSON equal to 3\nfull value was: true") assertFails(t, []byte("[1,2]"), JSONEqual(3), "expected: JSON equal to 3\nfull value was: [1,2]") }) t.Run("inequality with detailed diff", func(t *testing.T) { assertFails(t, `{"a":1,"b":3}`, JSONStrEqual(`{"a":1,"b":2}`), `JSON values at "b": expected = 2, actual = 3`+ "\n"+`full value was: {"a":1,"b":3}`) }) } func TestJSONProperty(t *testing.T) { assertPasses(t, []byte(`{"a":1,"b":2}`), JSONProperty("b").Should(Equal(2))) assertFails(t, []byte(`{"a":1,"b":2}`), JSONProperty("b").Should(Equal(3)), `JSON property "b" did not equal 3`+"\n"+`full value was: {"a":1,"b":2}`) assertFails(t, []byte(`{"a":1,"b":2}`), JSONProperty("c").Should(Equal(3)), `JSON property "c" not found`+"\n"+`full value was: {"a":1,"b":2}`) } func TestJSONOptProperty(t *testing.T) { assertPasses(t, []byte(`{"a":1,"b":2}`), JSONOptProperty("b").Should(Equal(2))) assertFails(t, []byte(`{"a":1,"b":2}`), JSONOptProperty("b").Should(Equal(3)), `JSON property "b" did not equal 3`+"\n"+`full value was: {"a":1,"b":2}`) assertFails(t, []byte(`{"a":1,"b":2}`), JSONOptProperty("c").Should(Equal(3)), `JSON property "c" did not equal 3`+"\n"+`full value was: {"a":1,"b":2}`) assertPasses(t, []byte(`{"a":1,"b":2}`), JSONOptProperty("c").Should(Equal(nil))) } func TestJSONArray(t *testing.T) { assertPasses(t, []byte(`[true, false]`), JSONArray().Should(Length().Should(Equal(2)))) assertPasses(t, []byte(`[true, false]`), JSONArray().Should(Items(Equal(true), Equal(false)))) assertFails(t, []byte(`{"a":1,"b":2}`), JSONArray().Should(Length().Should(Equal(2))), `wanted a JSON array but found a different type`) } func TestJSONMap(t *testing.T) { assertPasses(t, []byte(`{"a":1,"b":2}`), JSONMap().Should(Length().Should(Equal(2)))) assertPasses(t, []byte(`{"a":1,"b":2}`), JSONMap().Should(ValueForKey("a").Should(Equal(1)))) assertFails(t, []byte(`[true, false]`), JSONMap().Should(Length().Should(Equal(2))), `wanted a JSON object but found a different type`) } go-test-helpers-3.0.2/matchers/package_info.go000066400000000000000000000021061430344127400213020ustar00rootroot00000000000000// Package matchers provides a flexible test assertion API similar to Java's Hamcrest. Matchers are // constructed separately from the values being tested, and can then be applied to any value, or // negated, or combined in various ways. // // This implementation is for Go 1.17 so it does not yet have generics. Instead, all matchers take // values of type interface{} and must explicitly cast the type if needed. The simplest way to // provide type safety is to use Matcher.EnsureType(). // // Examples of syntax: // // import m "github.com/launchdarkly/go-test-helpers/matchers" // // func TestSomething(t *T) { // eventData := []string{ // `{"kind": "feature", "value": true}`, // `{"key": "x", "kind": "custom"}`, // } // m.For(t, "event data").Assert(eventData, m.ItemsInAnyOrder( // m.JSONStrEqual(`{"kind": "custom", "key": "x"}`), // m.JSONStrEqual(`{"kind": "feature", "value": true}`), // )) // m.For(t, "first event").Assert(eventData[0], // m.JSONProperty("kind").Should(m.Not(m.Equal("summary")))) // } package matchers go-test-helpers-3.0.2/matchers/strings.go000066400000000000000000000024371430344127400203740ustar00rootroot00000000000000package matchers import ( "fmt" "strings" ) // StringContains is a matcher for string values that tests for the presence of a substring, // case-sensitively. func StringContains(substring string) Matcher { return New( func(value interface{}) bool { return strings.Contains(value.(string), substring) }, func() string { return fmt.Sprintf("contains %q", substring) }, func(interface{}) string { return fmt.Sprintf("did not contain %q", substring) }, ).EnsureType("") } // StringHasPrefix is a matcher for string values that calls strings.HasPrefix. func StringHasPrefix(prefix string) Matcher { return New( func(value interface{}) bool { return strings.HasPrefix(value.(string), prefix) }, func() string { return fmt.Sprintf("starts with %q", prefix) }, func(interface{}) string { return fmt.Sprintf("did not start with %q", prefix) }, ).EnsureType("") } // StringHasSuffix is a matcher for string values that calls strings.HasSuffix. func StringHasSuffix(suffix string) Matcher { return New( func(value interface{}) bool { return strings.HasSuffix(value.(string), suffix) }, func() string { return fmt.Sprintf("ends with %q", suffix) }, func(interface{}) string { return fmt.Sprintf("did not end with %q", suffix) }, ).EnsureType("") } go-test-helpers-3.0.2/matchers/strings_test.go000066400000000000000000000015731430344127400214330ustar00rootroot00000000000000package matchers import "testing" func TestStringContains(t *testing.T) { assertPasses(t, "abc", StringContains("b")) assertFails(t, "abc", StringContains("x"), `did not contain "x"`+"\n"+`full value was: "abc"`) assertFails(t, "abc", StringContains("B"), `did not contain "B"`+"\n"+`full value was: "abc"`) } func TestStringHasPrefix(t *testing.T) { assertPasses(t, "abc", StringHasPrefix("a")) assertFails(t, "abc", StringHasPrefix("x"), `did not start with "x"`+"\n"+`full value was: "abc"`) assertFails(t, "abc", StringHasPrefix("A"), `did not start with "A"`+"\n"+`full value was: "abc"`) } func TestStringHasSuffix(t *testing.T) { assertPasses(t, "abc", StringHasSuffix("c")) assertFails(t, "abc", StringHasSuffix("x"), `did not end with "x"`+"\n"+`full value was: "abc"`) assertFails(t, "abc", StringHasSuffix("C"), `did not end with "C"`+"\n"+`full value was: "abc"`) } go-test-helpers-3.0.2/matchers/transform.go000066400000000000000000000074301430344127400207140ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // MatcherTransform is a combinator that allows an input value to be transformed to some // other value (possibly of a different type) before being tested by other Matchers. // // For instance, this could be used to access a field inside a struct or some other nested // data structure. Assuming there is a struct type S with a field F, you could do this: // // SF := matchers.Transform("F", // func(value interface{}) interface{} { return value.(S).F }) // SF.Should(Equal(3)).Assert(t, someInstanceOfS) // // The advantages of doing this, instead of simply getting the F field directly and // testing it, are 1. you can use combinators such as AllOf to test multiple properties // in a single assertion, and 2. failure messages will automatically include both a full // description of someInstanceOfS and an explanation of what was wrong with it. For // instance, in the example above, if someInstanceOfS.F was really 4, the failure message // would show: // // expected: F equal to 3 // actual value was: {F: 4} // // You can use MatcherTransform's other methods to add type safety. type MatcherTransform struct { name string getValue func(interface{}) (interface{}, error) expectedType interface{} } // Transform creates a MatcherTransform. The name parameter is a brief description of what // the output value is in relation to the input value (for instance, if you are getting a // field called F from a struct, it could simply be "F"); it will be prefixed to the // description of any Matcher that you use with Should(). The getValue parameter is a // function that transforms the original value into the value you will be testing. func Transform( name string, getValue func(interface{}) (interface{}, error), ) MatcherTransform { return MatcherTransform{name: name, getValue: getValue} } // EnsureInputValueType is the equivalent of Matcher.EnsureValueType. Given any value of // the desired type, it returns a modified MatcherTransform that will safely fail if the // wrong type is passed in. // // stringLength := matchers.Transform("string length", // func(value interface{}) interface{} { return len(value.(string)) }). // EnsureInputValueType("") func (mt MatcherTransform) EnsureInputValueType(valueOfType interface{}) MatcherTransform { mt.expectedType = valueOfType return mt } // Should applies a Matcher to the transformed value. That is, assuming that this MatcherTransform // converts an A value into a B value, mt.Should(Equal(3)) returns a Matcher that takes A, // converts it to B, and applies Equal(3) to B. func (mt MatcherTransform) Should(matcher Matcher) Matcher { if mt.getValue == nil { mt.getValue = func(value interface{}) (interface{}, error) { return value, nil } } return New( func(value interface{}) bool { newValue, err := mt.getValue(value) if err != nil { return false } return matcher.test(newValue) }, func() string { ret := mt.name if ret != "" { ret += " " } return ret + matcher.describeTest() }, func(value interface{}) string { newValue, err := mt.getValue(value) if err != nil { return err.Error() } ret := mt.name if ret != "" { ret += " " } return ret + matcher.describeFailure(newValue) }, ).EnsureType(mt.expectedType) } // Length is a MatcherTransform that takes any value len() can operate on, gets its // length, and applies some matcher to the result. func Length() MatcherTransform { return Transform( "length", func(value interface{}) (interface{}, error) { v := reflect.ValueOf(value) t := v.Type() switch t.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return v.Len(), nil default: return nil, fmt.Errorf("matchers.Length() was used for an inapplicable type (%T)", value) } }, ) } go-test-helpers-3.0.2/matchers/transform_test.go000066400000000000000000000031531430344127400217510ustar00rootroot00000000000000package matchers import ( "errors" "strings" "testing" ) func stringLength() MatcherTransform { return Transform( "string length", func(value interface{}) (interface{}, error) { return len(value.(string)), nil }, ) } func TestTransform(t *testing.T) { m := stringLength().Should(Equal(3)) assertPasses(t, "abc", m) assertFails(t, "abcd", m, `string length did not equal 3`+"\n"+`full value was: "abcd"`) } func TestTransformEnsureType(t *testing.T) { m := stringLength().EnsureInputValueType("example string"). Should(Equal(3)) assertPasses(t, "abc", m) assertFails(t, "abcd", m, `string length did not equal 3`+"\n"+`full value was: "abcd"`) assertFails(t, 3, m, "expected value of type string, was int\nfull value was: 3") } func TestTransformError(t *testing.T) { stringLengthForLowercaseStringsOnly := Transform( "string length", func(value interface{}) (interface{}, error) { if strings.ToLower(value.(string)) == value.(string) { return len(value.(string)), nil } return 0, errors.New("was not lowercase") }, ) m := stringLengthForLowercaseStringsOnly.Should(Equal(3)) assertPasses(t, "abc", m) assertFails(t, "Abc", m, `was not lowercase`+"\n"+`full value was: "Abc"`) } func TestLength(t *testing.T) { assertPasses(t, [3]int{7, 8, 9}, Length().Should(Equal(3))) assertPasses(t, []int{7, 8, 9}, Length().Should(Equal(3))) assertPasses(t, "abc", Length().Should(Equal(3))) assertPasses(t, map[string]int{"a": 1, "b": 2}, Length().Should(Equal(2))) assertFails(t, 3, Length().Should(Equal(3)), "matchers.Length() was used for an inapplicable type (int)\nfull value was: 3") } go-test-helpers-3.0.2/matchers/value_formatting.go000066400000000000000000000055511430344127400222510ustar00rootroot00000000000000package matchers import ( "encoding/json" "fmt" "reflect" "strings" "github.com/launchdarkly/go-test-helpers/v3/jsonhelpers" ) // DescribeValue tries to create attractive string representations of values for test // failure messages. The logic is as follows (whichever comes first): // // If the value is nil, it returns "nil". // // If the type is a struct that has "json" field tags, it is converted to JSON. // // If the type implements fmt.Stringer, its String method is called. // // If the type is string, it is quoted, unless it already has bracket or brace delimiters. // // If the type is []byte, it is converted to a string unchanged, unless it is valid JSON // in which case it is passed to jsonhelpers.CanonicalizeJSON. // // If the type is json.RawMessage, it is passed to jsonhelpers.CanonicalizeJSON. // // If the type is a slice or array, it is formatted as [value1, value2, value3] (unlike // Go's default formatting which has no commas) and each value is recursively formatted // with DescribeValue. // // At last resort, it is formatted with fmt.Sprintf("%+v"). func DescribeValue(value interface{}) string { if value == nil { return "nil" } if isJSONTaggedStruct(value) { return string(jsonhelpers.CanonicalizeJSON(jsonhelpers.ToJSON(value))) } switch v := value.(type) { case fmt.Stringer: return v.String() case string: if strings.HasPrefix(v, "{") && strings.HasSuffix(v, "}") { return v } if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") { return v } return `"` + v + `"` case []byte: return string(jsonhelpers.CanonicalizeJSON(v)) case json.RawMessage: return string(jsonhelpers.CanonicalizeJSON(v)) default: rv := reflect.ValueOf(value) if rv.Type().Kind() == reflect.Array || rv.Type().Kind() == reflect.Slice { parts := make([]string, 0, rv.Len()) for i := 0; i < rv.Len(); i++ { parts = append(parts, DescribeValue(rv.Index(i).Interface())) } return "[" + strings.Join(parts, ", ") + "]" } return fmt.Sprintf("%+v", value) } } func isJSONTaggedStruct(value interface{}) bool { t := reflect.TypeOf(value) if t.Kind() != reflect.Struct { return false } for i := 0; i < t.NumField(); i++ { field := t.Field(i) if field.PkgPath != "" { continue // field is not exported } tagStr := field.Tag.Get("json") if tagStr != "" { return true } } return false } func describeMatchers(matchers []Matcher, separator string) string { if len(matchers) == 1 { return matchers[0].describeTest() } parts := make([]string, 0, len(matchers)) for _, m := range matchers { parts = append(parts, "("+m.describeTest()+")") } return strings.Join(parts, separator) } func describeFailures(matchers []Matcher, value interface{}) string { var fails []string for _, m := range matchers { if !m.test(value) { fails = append(fails, m.describeFailure(value)) } } return strings.Join(fails, ", ") } go-test-helpers-3.0.2/matchers/value_formatting_test.go000066400000000000000000000027531430344127400233110ustar00rootroot00000000000000package matchers import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestValueFormatting(t *testing.T) { assert.Equal(t, `"abc"`, DescribeValue("abc")) assert.Equal(t, `{abc}`, DescribeValue("{abc}")) assert.Equal(t, `[abc]`, DescribeValue("[abc]")) assert.Equal(t, decorate("abc"), DescribeValue(decoratedString("abc"))) assert.Equal(t, "abc", DescribeValue([]byte("abc"))) assert.Equal(t, `{"a":1,"b":2}`, DescribeValue([]byte(`{"b":2,"a":1}`))) assert.Equal(t, `{"a":1,"b":2}`, DescribeValue(json.RawMessage(`{"b":2,"a":1}`))) taggedStruct := struct { Name string `json:"name"` Values []int `json:"values"` }{"Lucy", []int{1, 2}} untaggedStruct := struct { Name string Values []int }{"Mina", []int{1, 2}} assert.Equal(t, `{"name":"Lucy","values":[1,2]}`, DescribeValue(taggedStruct)) assert.Equal(t, `{Name:Mina Values:[1 2]}`, DescribeValue(untaggedStruct)) assert.Equal(t, `[1, 2]`, DescribeValue([]int{1, 2})) assert.Equal(t, `[1, 2]`, DescribeValue([2]int{1, 2})) assert.Equal(t, `["a", "b"]`, DescribeValue([]string{"a", "b"})) } func TestSomething(t *testing.T) { eventData := []string{ `{"kind": "feature", "value": true}`, `{"key": "x", "kind": "custom"}`, } For(t, "event data").Assert(eventData, ItemsInAnyOrder( JSONStrEqual(`{"kind": "custom", "key": "x"}`), JSONStrEqual(`{"kind": "feature", "value": true}`), )) For(t, "first event").Assert(eventData[0], JSONProperty("kind").Should(Not(Equal("summary")))) } go-test-helpers-3.0.2/package_info.go000066400000000000000000000002231430344127400174720ustar00rootroot00000000000000// Package helpers (github.com/launchdarkly/go-test-helpers) contains miscellaneous convenience functions // for use in test code. package helpers go-test-helpers-3.0.2/pointers.go000066400000000000000000000001631430344127400167320ustar00rootroot00000000000000package helpers // AsPointer returns a pointer to a copy of a value. func AsPointer[V any](v V) *V { return &v } go-test-helpers-3.0.2/pointers_test.go000066400000000000000000000004561430344127400177760ustar00rootroot00000000000000package helpers import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAsPtr(t *testing.T) { boolP := AsPointer(true) require.NotNil(t, boolP) assert.True(t, *boolP) intP := AsPointer(2) require.NotNil(t, intP) assert.Equal(t, 2, *intP) } go-test-helpers-3.0.2/testbox/000077500000000000000000000000001430344127400162305ustar00rootroot00000000000000go-test-helpers-3.0.2/testbox/interface.go000066400000000000000000000063211430344127400205210ustar00rootroot00000000000000package testbox import "github.com/stretchr/testify/require" // TestingT is a subset of the testing.T interface that allows tests to run in either a real test // context, or a mock test scope that is decoupled from the regular testing framework (SandboxTest). // // This may be useful in a scenario where you have a contract test that verifies the behavior of some // unknown interface implementation. In order to verify that the contract test is reliable, you could // create implementations that either adhere to the contract or deliberately break it, run the contract // test against those, and verify that the test fails if and only if it should fail. // // The reason this cannot be done with the Go testing package alone is that the standard testing.T type // cannot be created from within test code; instances are always passed in from the test framework. // Therefore, the contract test would have to be run against the actual *testing.T instance that // belongs to the test-of-the-test, and if it failed in a situation when we actually wanted it to // to fail, that would be incorrectly reported as a failure of the test-of-the-test. // // To work around this limitation of the testing package, this package provides a TestingT interface // that has two implementations: real and mock. Test logic can then be written against this TestingT, // rather than *testing.T. // // func RunContractTests(t *testing.T, impl InterfaceUnderTest) { // runContractTests(testbox.RealTest(t)) // } // // func runContractTests(abstractT testbox.TestingT, impl InterfaceUnderTest) { // assert.True(abstractT, impl.SomeConditionThatShouldBeTrueForTheseInputs(someParams)) // abstractT.Run("subtest", func(abstractSubT helpers.TestingT) { ... } // } // // func TestContractTestFailureCondition(t *testing.T) { // impl := createDeliberatelyBrokenImplementation() // result := testbox.SandboxTest(func(abstractT testbox.TestingT) { // runContractTests(abstractT, impl) }) // assert.True(t, result.Failed // we expect it to fail // assert.Len(t, result.Failures, 1) // } // // TestingT includes the same subsets of testing.T methods that are defined in the TestingT interfaces // of github.com/stretchr/testify/assert and github.com/stretchr/testify/require, so all assertions in // those packages will work. It also provides Run, Skip, and SkipNow. It does not support Parallel. type TestingT interface { require.TestingT // Run runs a subtest with a new TestingT that applies only to the scope of the subtest. It is // equivalent to the same method in testing.T, except the subtest takes a parameter of type TestingT // instead of *testing.T. // // If the subtest fails, the parent test also fails, but FailNow and SkipNow on the subtest do not // cause the parent test to exit early. Run(name string, action func(TestingT)) // Failed tells whether whether any assertions in the test have failed so far. It is equivalent to // the same method in testing.T. Failed() bool // Skip marks the test as skipped and exits early, logging a message. It is equivalent to the same // method in testing.T. Skip(args ...interface{}) // SkipNow marks the test as skipped and exits early. It is equivalent to the same method in testing.T. SkipNow() } go-test-helpers-3.0.2/testbox/package_info.go000066400000000000000000000002641430344127400211670ustar00rootroot00000000000000// Package testbox provides the ability to run test logic that uses a subset of Go's testing.T // methods either inside or outside the regular testing environment. package testbox go-test-helpers-3.0.2/testbox/real.go000066400000000000000000000016021430344127400175010ustar00rootroot00000000000000package testbox import "testing" type realTestingT struct { t *testing.T } // RealTest provides an implementation of TestingT for running test logic in a regular test context. // // See TestingT for details. func RealTest(t *testing.T) TestingT { return realTestingT{t} } func (r realTestingT) Errorf(format string, args ...interface{}) { r.t.Errorf(format, args...) // COVERAGE: can't do this in test_sandbox_test; it'll cause a real failure } func (r realTestingT) Run(name string, action func(TestingT)) { r.t.Run(name, func(tt *testing.T) { action(realTestingT{tt}) }) } func (r realTestingT) FailNow() { r.t.FailNow() // COVERAGE: can't do this in test_sandbox_test; it'll cause a real failure } func (r realTestingT) Failed() bool { return r.t.Failed() } func (r realTestingT) Skip(args ...interface{}) { r.t.Skip(args...) } func (r realTestingT) SkipNow() { r.t.SkipNow() } go-test-helpers-3.0.2/testbox/real_test.go000066400000000000000000000027761430344127400205550ustar00rootroot00000000000000package testbox import ( "testing" "github.com/stretchr/testify/assert" ) func TestRealTest(t *testing.T) { // can't test failure cases, since then *this* test would fail t.Run("success", func(t *testing.T) { rt := RealTest(t) assert.True(rt, true) assert.False(t, rt.Failed()) assert.False(t, t.Failed()) assert.False(t, t.Skipped()) }) t.Run("subtest success", func(t *testing.T) { ran := false rt := RealTest(t) rt.Run("sub", func(u TestingT) { ran = true assert.True(u, true) }) assert.True(t, ran) assert.False(t, rt.Failed()) assert.False(t, t.Failed()) assert.False(t, t.Skipped()) }) t.Run("skip", func(t *testing.T) { // this test will always be reported as skipped rt := RealTest(t) rt.Skip() assert.True(t, false) // won't execute because we exited early on Skip }) t.Run("subtest skip", func(t *testing.T) { ran := false continued := false rt := RealTest(t) rt.Run("sub", func(u TestingT) { ran = true u.Skip("let's skip this") continued = true }) assert.True(t, ran) assert.False(t, continued) assert.False(t, rt.Failed()) assert.False(t, t.Failed()) assert.False(t, t.Skipped()) }) t.Run("subtest SkipNow", func(t *testing.T) { ran := false continued := false rt := RealTest(t) rt.Run("sub", func(u TestingT) { ran = true u.SkipNow() continued = true }) assert.True(t, ran) assert.False(t, continued) assert.False(t, rt.Failed()) assert.False(t, t.Failed()) assert.False(t, t.Skipped()) }) } go-test-helpers-3.0.2/testbox/sandbox.go000066400000000000000000000130321430344127400202140ustar00rootroot00000000000000package testbox import ( "fmt" "runtime" "strings" "sync" "github.com/stretchr/testify/assert" ) // SandboxResult describes the aggregate test state produced by calling SandboxTest. type SandboxResult struct { // True if any failures were reported during SandboxTest. Failed bool // True if the test run with SandboxTest called Skip or SkipNow. This is only true if // the top-level TestingT was skipped, not any subtests. Skipped bool // All failures logged during SandboxTest, including subtests. Failures []LogItem // All tests that were skipped during SandboxTest, including subtests. Skips []LogItem } type testState struct { failed bool skipped bool failures []LogItem skips []LogItem } // TestPath identifies the level of test that failed or skipped. SandboxResult.Failures and // SandboxResult.Skips use this type to distinguish between the top-level test that was run with // SandboxTest and subtests that were run within that test with TestingT.Run(). A nil value means the // top-level test; a single string element is the name of a subtest run from the top level with // TestingT.Run(); nested subtests add an element for each level. type TestPath []string // LogItem describes either a failed assertion or a skip that happened during SandboxTest. type LogItem struct { // Path identifies the level of test that failed or was skipped. Path TestPath // Message is the failure message or skip message, if any. It is the result of calling fmt.Sprintf // or Sprintln on the arguments that were passed to TestingT.Errorf or TestingT.Skip. If a test // failed without specifying a message, this is "". Message string } type mockTestingT struct { testState path TestPath lock sync.Mutex } // SandboxTest runs a test function against a TestingT instance that applies only to the scope of // that test. If the function makes a failed assertion, marks the test as skipped, or forces an early // exit with FailNow or SkipNow, this is reflected in the SandboxResult but does not affect the state // of the regular test framework (assuming that this code is even executing within a Go test; it does // not have to be). // // The reason this uses a callback function parameter, rather than simply having the SandboxResult // implement TestingT itself, is that the function must be run on a separate goroutine so that // the sandbox can intercept any early exits from FailNow or SkipNow. // // SandboxTest does not recover from panics. // // See TestingT for more details. func SandboxTest(action func(TestingT)) SandboxResult { sub := new(mockTestingT) sub.runSafely(action) state := sub.getState() return SandboxResult{ Failed: state.failed, Skipped: state.skipped, Failures: state.failures, Skips: state.skips, } } func (m *mockTestingT) Errorf(format string, args ...interface{}) { m.lock.Lock() defer m.lock.Unlock() m.failed = true m.failures = append(m.failures, LogItem{Path: m.path, Message: fmt.Sprintf(format, args...)}) } func (m *mockTestingT) Run(name string, action func(TestingT)) { sub := &mockTestingT{path: append(m.path, name)} sub.runSafely(action) subState := sub.getState() m.lock.Lock() defer m.lock.Unlock() m.failed = m.failed || subState.failed m.failures = append(m.failures, subState.failures...) m.skips = append(m.skips, subState.skips...) } func (m *mockTestingT) FailNow() { m.lock.Lock() defer m.lock.Unlock() m.testState.failed = true runtime.Goexit() } func (m *mockTestingT) Failed() bool { m.lock.Lock() defer m.lock.Unlock() return m.failed } func (m *mockTestingT) Skip(args ...interface{}) { m.lock.Lock() defer m.lock.Unlock() m.skipped = true m.skips = append(m.skips, LogItem{Path: m.path, Message: strings.TrimSuffix(fmt.Sprintln(args...), "\n")}) runtime.Goexit() } func (m *mockTestingT) SkipNow() { m.Skip() } func (m *mockTestingT) getState() testState { m.lock.Lock() defer m.lock.Unlock() ret := testState{failed: m.failed, skipped: m.skipped} if len(m.failures) > 0 { ret.failures = make([]LogItem, len(m.failures)) copy(ret.failures, m.failures) } if len(m.skips) > 0 { ret.skips = make([]LogItem, len(m.skips)) copy(ret.skips, m.skips) } return ret } func (m *mockTestingT) runSafely(action func(TestingT)) { exited := make(chan struct{}, 1) go func() { defer func() { close(exited) }() action(m) }() <-exited } // ShouldFail is a shortcut for running some action against a testbox.TestingT and // asserting that it failed. func ShouldFail(t assert.TestingT, action func(TestingT)) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } shouldGetHere := make(chan struct{}, 1) result := SandboxTest(func(t1 TestingT) { action(t1) shouldGetHere <- struct{}{} }) if !result.Failed { t.Errorf("expected test to fail, but it passed") return false } if len(shouldGetHere) == 0 { t.Errorf("test failed as expected, but it also terminated early and should not have") return false } return true } // ShouldFailAndExitEarly is the same as ShouldFail, except that it also asserts that // the test was terminated early with FailNow. func ShouldFailAndExitEarly(t assert.TestingT, action func(TestingT)) bool { if t, ok := t.(interface{ Helper() }); ok { t.Helper() } shouldNotGetHere := make(chan struct{}, 1) result := SandboxTest(func(t1 TestingT) { action(t1) shouldNotGetHere <- struct{}{} }) if !result.Failed { t.Errorf("expected test to fail, but it passed") return false } if len(shouldNotGetHere) != 0 { t.Errorf("test failed as expected, but it should have also terminated early and did not") return false } return true } go-test-helpers-3.0.2/testbox/sandbox_test.go000066400000000000000000000130641430344127400212600ustar00rootroot00000000000000package testbox import ( "testing" "github.com/stretchr/testify/assert" ) func TestSandboxTest(t *testing.T) { t.Run("success", func(t *testing.T) { r := SandboxTest(func(u TestingT) { assert.True(u, true) }) assert.False(t, r.Failed) assert.Len(t, r.Failures, 0) assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("failure", func(t *testing.T) { r := SandboxTest(func(u TestingT) { assert.False(t, u.Failed()) assert.Equal(u, "abc", "def") assert.Fail(u, "another message") assert.True(t, u.Failed()) }) assert.True(t, r.Failed) if assert.Len(t, r.Failures, 2) { assert.Nil(t, r.Failures[0].Path) assert.Contains(t, r.Failures[0].Message, "abc") assert.Nil(t, r.Failures[1].Path) assert.Contains(t, r.Failures[1].Message, "another") } assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("FailNow", func(t *testing.T) { continued := false r := SandboxTest(func(u TestingT) { u.FailNow() continued = true }) assert.True(t, r.Failed) assert.Len(t, r.Failures, 0) assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) assert.False(t, continued) }) t.Run("skip", func(t *testing.T) { ran := false continued := false r := SandboxTest(func(u TestingT) { ran = true u.Skip("please", "skip") continued = true }) assert.True(t, ran) assert.False(t, continued) assert.False(t, r.Failed) assert.Len(t, r.Failures, 0) assert.True(t, r.Skipped) if assert.Len(t, r.Skips, 1) { assert.Nil(t, r.Skips[0].Path) assert.Equal(t, "please skip", r.Skips[0].Message) } }) t.Run("SkipNow", func(t *testing.T) { ran := false continued := false r := SandboxTest(func(u TestingT) { ran = true u.SkipNow() continued = true }) assert.True(t, ran) assert.False(t, continued) assert.False(t, r.Failed) assert.Len(t, r.Failures, 0) assert.True(t, r.Skipped) if assert.Len(t, r.Skips, 1) { assert.Nil(t, r.Skips[0].Path) assert.Equal(t, "", r.Skips[0].Message) } }) } func TestSandboxTestSubtests(t *testing.T) { t.Run("successes", func(t *testing.T) { r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { assert.True(uu, true) }) u.Run("sub2", func(uu TestingT) { assert.True(uu, true) }) }) assert.False(t, r.Failed) assert.Len(t, r.Failures, 0) assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("failures", func(t *testing.T) { r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { assert.Equal(uu, "abc", "def") }) u.Run("sub2", func(uu TestingT) { assert.Equal(uu, "ghi", "jkl") }) }) assert.True(t, r.Failed) if assert.Len(t, r.Failures, 2) { assert.Equal(t, TestPath{"sub1"}, r.Failures[0].Path) assert.Contains(t, r.Failures[0].Message, "abc") assert.Equal(t, TestPath{"sub2"}, r.Failures[1].Path) assert.Contains(t, r.Failures[1].Message, "ghi") } assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("successes", func(t *testing.T) { r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { assert.True(uu, true) }) u.Run("sub2", func(uu TestingT) { assert.True(uu, true) }) }) assert.False(t, r.Failed) assert.Len(t, r.Failures, 0) assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("FailNow", func(t *testing.T) { continued1 := false ran2 := false r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { assert.True(uu, false) uu.FailNow() // equivalent to require.True(uu, false) assert.False(uu, true) // we shouldn't get here continued1 = true }) u.Run("sub2", func(uu TestingT) { ran2 = true }) }) assert.False(t, continued1) assert.True(t, ran2) assert.True(t, r.Failed) if assert.Len(t, r.Failures, 1) { assert.Equal(t, TestPath{"sub1"}, r.Failures[0].Path) } assert.False(t, r.Skipped) assert.Len(t, r.Skips, 0) }) t.Run("skip", func(t *testing.T) { continued1 := false ran2 := false r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { uu.Skip("please", "skip") continued1 = true }) u.Run("sub2", func(uu TestingT) { ran2 = true }) }) assert.False(t, continued1) assert.True(t, ran2) assert.False(t, r.Failed) assert.False(t, r.Skipped) if assert.Len(t, r.Skips, 1) { assert.Equal(t, TestPath{"sub1"}, r.Skips[0].Path) assert.Equal(t, "please skip", r.Skips[0].Message) } }) t.Run("SkipNow", func(t *testing.T) { continued1 := false ran2 := false r := SandboxTest(func(u TestingT) { u.Run("sub1", func(uu TestingT) { uu.SkipNow() continued1 = true // we shouldn't get here }) u.Run("sub2", func(uu TestingT) { ran2 = true }) }) assert.False(t, continued1) assert.True(t, ran2) assert.False(t, r.Failed) assert.False(t, r.Skipped) if assert.Len(t, r.Skips, 1) { assert.Equal(t, TestPath{"sub1"}, r.Skips[0].Path) assert.Equal(t, "", r.Skips[0].Message) } }) } func TestShouldFail(t *testing.T) { ShouldFail(t, func(t TestingT) { t.Errorf("boo") }) result := SandboxTest(func(t TestingT) { ShouldFail(t, func(TestingT) {}) }) assert.True(t, result.Failed) result = SandboxTest(func(t TestingT) { ShouldFail(t, func(TestingT) { t.Errorf("boo") t.FailNow() // unexpected early exit }) }) assert.True(t, result.Failed) ShouldFailAndExitEarly(t, func(t TestingT) { t.Errorf("boo") t.FailNow() }) result = SandboxTest(func(t TestingT) { ShouldFailAndExitEarly(t, func(TestingT) { t.Errorf("boo") }) }) assert.True(t, result.Failed) }