pax_global_header00006660000000000000000000000064145741370560014526gustar00rootroot0000000000000052 comment=7cb47895fc5081e3539ce5cf19f49416b705a4fb onpar-0.3.3/000077500000000000000000000000001457413705600126505ustar00rootroot00000000000000onpar-0.3.3/.github/000077500000000000000000000000001457413705600142105ustar00rootroot00000000000000onpar-0.3.3/.github/workflows/000077500000000000000000000000001457413705600162455ustar00rootroot00000000000000onpar-0.3.3/.github/workflows/unit-test.yml000066400000000000000000000014271457413705600207300ustar00rootroot00000000000000name: unit test on: push: branches: - main pull_request: branches: - main jobs: test: runs-on: ubuntu-latest env: FORCE_COLOR: 1 steps: - uses: earthly/actions/setup-earthly@v1 with: version: v0.7.2 - uses: actions/checkout@v2 - name: Put back the git branch into git (Earthly uses it for tagging) run: | branch="" if [ -n "$GITHUB_HEAD_REF" ]; then branch="$GITHUB_HEAD_REF" else branch="${GITHUB_REF##*/}" fi git checkout -b "$branch" || true - name: Earthly version run: earthly --version - name: Run tests run: earthly +test onpar-0.3.3/.travis.yml000066400000000000000000000001151457413705600147560ustar00rootroot00000000000000language: go go: - 1.14.x - 1.15.x install: - go get -d -v -t ./... onpar-0.3.3/Earthfile000066400000000000000000000036071457413705600145040ustar00rootroot00000000000000VERSION 0.7 # Make sure these are up to date ARG goversion=1.21 ARG distro=alpine3.17 FROM golang:${goversion}-${distro} WORKDIR /go-workdir deps: # Copying only go.mod and go.sum at first allows earthly to cache the result of # `go mod download` unless go.mod or go.sum have changed. COPY go.mod go.sum . RUN go mod download # `go mod download` may have changed these files, so we save them locally when # `earthly +deps` is called directly. SAVE ARTIFACT go.mod AS LOCAL go.mod SAVE ARTIFACT go.sum AS LOCAL go.sum test-base: FROM +deps # gcc and g++ are required for `go test -race` RUN apk add --update gcc g++ COPY . . project-base: FROM +deps COPY . . # tidy runs 'go mod tidy' and updates go.mod and go.sum. tidy: FROM +project-base RUN go mod tidy SAVE ARTIFACT go.mod AS LOCAL go.mod SAVE ARTIFACT go.sum AS LOCAL go.sum # vet runs 'go vet'. vet: FROM +project-base # pkg is the package to run 'go vet' against. ARG pkg = ./... RUN go vet $path # fmt formats source files. fmt: FROM +project-base RUN go install golang.org/x/tools/cmd/goimports@latest RUN goimports -w . FOR --sep "\n" gofile IN $(find . -name \'*.go\') SAVE ARTIFACT $gofile AS LOCAL $gofile END # generate re-generates code and saves the outputs locally. generate: FROM +project-base RUN go install git.sr.ht/~nelsam/hel@latest RUN go install golang.org/x/tools/cmd/goimports@latest RUN go generate ./... # 'go install' can update our go.mod and go.sum, so save them too. SAVE ARTIFACT go.mod AS LOCAL go.mod SAVE ARTIFACT go.sum AS LOCAL go.sum FOR gofile IN $(find . -name helheim_test.go) SAVE ARTIFACT $gofile AS LOCAL $gofile END # test runs tests. test: FROM +test-base # pkg is the package to run tests against. ARG pkg = ./... RUN go test -race $pkg onpar-0.3.3/LICENSE000066400000000000000000000020601457413705600136530ustar00rootroot00000000000000MIT License Copyright (c) 2016 Andrew Poydence Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. onpar-0.3.3/README.md000066400000000000000000000162611457413705600141350ustar00rootroot00000000000000# onpar [![docs][pkg-docs-badge]][pkg-docs] [![gha][gha-badge]][gha] Parallel testing framework for Go ## Goals - Provide structured testing, with per-spec setup and teardown. - Discourage using closure state to share memory between setup/spec/teardown functions. - Sharing memory between the steps of a spec by using closure state means that you're also sharing memory with _other tests_. This often results in test pollution. - Run tests in parallel by default. - Most of the time, well-written unit tests are perfectly capable of running in parallel, and sometimes running tests in parallel can uncover extra bugs. This should be the default. - Work within standard go test functions, simply wrapping standard `t.Run` semantics. - `onpar` should not feel utterly alien to people used to standard go testing. It does some extra work to allow structured tests, but for the most part it isn't hiding any complicated logic - it mostly just calls `t.Run`. Onpar provides a BDD style of testing, similar to what you might find with something like ginkgo or goconvey. The biggest difference between onpar and its peers is that a `BeforeEach` function in `onpar` may return a value, and that value will become the parameter required in child calls to `Spec`, `AfterEach`, and `BeforeEach`. This allows you to write tests that share memory between `BeforeEach`, `Spec`, and `AfterEach` functions _without sharing memory with other tests_. When used properly, this makes test pollution nearly impossible and makes it harder to write flaky tests. ## Running After constructing a top-level `*Onpar`, `defer o.Run()`. If `o.Run()` is never called, the test will panic during `t.Cleanup`. This is to prevent false passes when `o.Run()` is accidentally omitted. ### Assertions OnPar provides an expectation library in the `expect` sub-package. Here is some more information about `Expect` and some of the matchers that are available: - [Expect](expect/README.md) - [Matchers](matchers/README.md) However, OnPar is not opinionated - any assertion library or framework may be used within specs. ### Specs Test assertions are done within a `Spec()` function. Each `Spec` has a name and a function with a single argument. The type of the argument is determined by how the suite was constructed: `New()` returns a suite that takes a `*testing.T`, while `BeforeEach` constructs a suite that takes the return type of the setup function. Each `Spec` is run in parallel (`t.Parallel()` is invoked for each spec before calling the given function). ```go func TestSpecs(t *testing.T) { type testContext struct { t *testing.T a int b float64 } o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) testContext { return testContext{t: t, a: 99, b: 101.0} }) defer o.Run() o.AfterEach(func(tt testContext) { // ... }) o.Spec("something informative", func(tt testContext) { if tt.a != 99 { tt.t.Errorf("%d != 99", tt.a) } }) } ``` ### Serial Specs While `onpar` is intended to heavily encourage running specs in parallel, we recognize that that's not always an option. Sometimes proper mocking is just too time consuming, or a singleton package is just too hard to replace with something better. For those times that you just can't get around the need for serial tests, we provide `SerialSpec`. It works exactly the same as `Spec`, except that onpar doesn't call `t.Parallel` before running it. ### Grouping `Group`s are used to keep `Spec`s in logical place. The intention is to gather each `Spec` in a reasonable place. Each `Group` may construct a new child suite using `BeforeEach`. ```go func TestGrouping(t *testing.T) { type topContext struct { t *testing.T a int b float64 } o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) topContext { return topContext{t: t, a: 99, b: 101} } defer o.Run() o.Group("some-group", func() { type groupContext struct { t *testing.T s string } o := onpar.BeforeEach(o, func(tt topContext) groupContext { return groupContext{t: tt.t, s: "foo"} }) o.AfterEach(func(tt groupContext) { // ... }) o.Spec("something informative", func(tt groupContext) { // ... }) }) } ``` ### Run Order Each `BeforeEach()` runs before any `Spec` in the same `Group`. It will also run before any sub-group `Spec`s and their `BeforeEach`es. Any `AfterEach()` will run after the `Spec` and before parent `AfterEach`es. ``` go func TestRunOrder(t *testing.T) { type topContext struct { t *testing.T i int s string } o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) topContext { // Spec "A": Order = 1 // Spec "B": Order = 1 // Spec "C": Order = 1 return topContext{t: t, i: 99, s: "foo"} }) defer o.Run() o.AfterEach(func(tt topContext) { // Spec "A": Order = 4 // Spec "B": Order = 6 // Spec "C": Order = 6 }) o.Group("DA", func() { o.AfterEach(func(tt topContext) { // Spec "A": Order = 3 // Spec "B": Order = 5 // Spec "C": Order = 5 }) o.Spec("A", func(tt topContext) { // Spec "A": Order = 2 }) o.Group("DB", func() { type dbContext struct { t *testing.T f float64 } o.BeforeEach(func(tt topContext) dbContext { // Spec "B": Order = 2 // Spec "C": Order = 2 return dbContext{t: tt.t, f: 101} }) o.AfterEach(func(tt dbContext) { // Spec "B": Order = 4 // Spec "C": Order = 4 }) o.Spec("B", func(tt dbContext) { // Spec "B": Order = 3 }) o.Spec("C", func(tt dbContext) { // Spec "C": Order = 3 }) }) o.Group("DC", func() { o.BeforeEach(func(tt topContext) *testing.T { // Will not be invoked (there are no specs) }) o.AfterEach(func(t *testing.T) { // Will not be invoked (there are no specs) }) }) }) } ``` ## Avoiding Closure Why bother with returning values from a `BeforeEach`? To avoid closure of course! When running `Spec`s in parallel (which they always do), each variable needs a new instance to avoid race conditions. If you use closure, then this gets tough. So onpar will pass the arguments to the given function returned by the `BeforeEach`. The `BeforeEach` is a gatekeeper for arguments. The returned values from `BeforeEach` are required for the following `Spec`s. Child `Group`s are also passed what their direct parent `BeforeEach` returns. [pkg-docs-badge]: https://pkg.go.dev/badge/github.com/poy/onpar.svg [pkg-docs]: https://pkg.go.dev/github.com/poy/onpar [gha-badge]: https://github.com/poy/onpar/actions/workflows/unit-test.yml/badge.svg [gha]: https://github.com/poy/onpar/actions/workflows/unit-test.yml onpar-0.3.3/diff/000077500000000000000000000000001457413705600135605ustar00rootroot00000000000000onpar-0.3.3/diff/base_string.go000066400000000000000000000017201457413705600164070ustar00rootroot00000000000000package diff import "github.com/poy/onpar/diff/str" type baseDiff struct { actual, expected []rune sections []str.DiffSection cost float64 } func (d *baseDiff) equal() bool { if len(d.actual) != len(d.expected) { return false } for i, ar := range d.actual { if ar != d.expected[i] { return false } } return true } func (d *baseDiff) calculate() { if d.sections != nil { return } d.sections = []str.DiffSection{{Actual: d.actual, Expected: d.expected}} if d.equal() { d.sections[0].Type = str.TypeMatch return } d.sections[0].Type = str.TypeReplace if len(d.actual) > len(d.expected) { d.cost = float64(len(d.actual)) return } d.cost = float64(len(d.expected)) } func (d *baseDiff) Cost() float64 { d.calculate() return d.cost } func (d *baseDiff) Sections() []str.DiffSection { d.calculate() return d.sections } func baseStringDiff(actual, expected []rune) str.Diff { return &baseDiff{actual: actual, expected: expected} } onpar-0.3.3/diff/diff.go000066400000000000000000000241051457413705600150210ustar00rootroot00000000000000package diff import ( "context" "fmt" "reflect" "strings" "sync" "time" "github.com/poy/onpar/diff/str" ) var DefaultStrDiffs = []StringDiffAlgorithm{str.NewCharDiff()} const DefaultTimeout = time.Second // StringDiffAlgorithm is a type which can generate diffs between two strings. type StringDiffAlgorithm interface { // Diffs takes a context.Context to know when to stop, returning a channel // of diffs. Each new diff returned on this channel should have a lower // cost than the previous one. // // If a higher cost diff is returned after a lower cost diff, it will be // discarded. // // Once ctx.Done() is closed, diffs will not be read off of the returned // channel - it's up to the algorithm to perform select statements to avoid // deadlocking. Diffs(ctx context.Context, actual, expected []rune) <-chan str.Diff } // WithStringAlgos picks the algorithms that will be used to diff strings. We // will always use a "dumb" algorithm for the base case that simply returns the // full string as either equal or different, but extra algorithms can be // provided to get more useful diffs for larger strings. For example, // StringCharDiff gets diffs of characters (good for catching misspellings), and // StringLineDiff gets diffs of lines (good for large multiline output). // // The default is DefaultStrDiffs. // // If called without any arguments, only the "dumb" algorithm will be used. func WithStringAlgos(algos ...StringDiffAlgorithm) Opt { return func(d Differ) Differ { d.stringAlgos = algos return d } } // WithTimeout sets a timeout for diffing. Normally, diffs will be refined until // the "cost" of the diff is as low as possible. If diffing is taking too long, // the best diff that has been loaded will be returned. // // The default is DefaultTimeout. // // If no diff at all has been generated yet, we will still wait until the first // diff is generated before returning, but no refinement will be done. func WithTimeout(timeout time.Duration) Opt { return func(d Differ) Differ { d.timeout = timeout return d } } // Opt is an option type that can be passed to New. // // Most of the time, you'll want to use at least one // of Actual or Expected, to differentiate the two // in your output. type Opt func(Differ) Differ // WithFormat returns an Opt that wraps up differences // using a format string. The format should contain // one '%s' to add the difference string in. func WithFormat(format string) Opt { return func(d Differ) Differ { d.wrappers = append(d.wrappers, func(v string) string { return fmt.Sprintf(format, v) }) return d } } // Sprinter is any type which can print a string. type Sprinter interface { Sprint(...any) string } // WithSprinter returns an Opt that wraps up differences // using a Sprinter. func WithSprinter(s Sprinter) Opt { return func(d Differ) Differ { d.wrappers = append(d.wrappers, func(v string) string { return s.Sprint(v) }) return d } } func applyOpts(o *Differ, opts ...Opt) { for _, opt := range opts { *o = opt(*o) } } // Actual returns an Opt that only applies formatting to the actual value. // Non-formatting options (e.g. different diffing algorithms) will have no // effect. func Actual(opts ...Opt) Opt { return func(d Differ) Differ { if d.actual == nil { d.actual = &Differ{} } applyOpts(d.actual, opts...) return d } } // Expected returns an Opt that only applies formatting to the expected value. // Non-formatting options (e.g. different diffing algorithms) will have no // effect. func Expected(opts ...Opt) Opt { return func(d Differ) Differ { if d.expected == nil { d.expected = &Differ{} } applyOpts(d.expected, opts...) return d } } // Differ is a type that can diff values. It keeps its own // diffing style. type Differ struct { wrappers []func(string) string actual *Differ expected *Differ timeout time.Duration stringAlgos []StringDiffAlgorithm } // New creates a Differ, using the passed in opts to manipulate // its diffing behavior and output. // // By default, we wrap mismatched text in angle brackets and separate them with // "!=". Example: // // matching text >actual!=expected< more matching text // // opts will be applied to the text in the order they // are passed in, so you can do things like color a value // and then wrap the colored text up in custom formatting. // // See the examples on the different Opt types for more // detail. func New(opts ...Opt) *Differ { d := Differ{ timeout: DefaultTimeout, stringAlgos: DefaultStrDiffs, } for _, o := range opts { d = o(d) } if d.needsDefaultFmt() { d = WithFormat(">%s<")(d) d = Actual(WithFormat("%s!="))(d) } return &d } func (d *Differ) needsDefaultFmt() bool { return len(d.wrappers) == 0 && d.actual == nil && d.expected == nil } // format is used to format a string using the wrapper functions. func (d Differ) format(v string) string { for _, w := range d.wrappers { v = w(v) } return v } // Diff takes two values and returns a string showing a // diff of them. func (d *Differ) Diff(actual, expected any) string { ctx, cancel := context.WithTimeout(context.Background(), d.timeout) defer cancel() return d.diff(ctx, reflect.ValueOf(actual), reflect.ValueOf(expected)) } func (d *Differ) genDiff(format string, actual, expected any) string { afmt := fmt.Sprintf(format, actual) if d.actual != nil { afmt = d.actual.format(afmt) } efmt := fmt.Sprintf(format, expected) if d.expected != nil { efmt = d.expected.format(efmt) } return d.format(afmt + efmt) } func (d *Differ) diff(ctx context.Context, av, ev reflect.Value) string { if !av.IsValid() { if !ev.IsValid() { return "" } if ev.Kind() == reflect.Ptr { return d.diff(ctx, av, ev.Elem()) } return d.genDiff("%v", "", ev.Interface()) } if !ev.IsValid() { if av.Kind() == reflect.Ptr { return d.diff(ctx, av.Elem(), ev) } return d.genDiff("%v", av.Interface(), "") } if av.Kind() != ev.Kind() { return d.genDiff("%s", av.Type(), ev.Type()) } if av.CanInterface() { switch av.Interface().(type) { case []rune, []byte, string: // TODO: we probably want to (eventually) run this concurrently. As // is, a struct with two complicated strings in two separate fields // that both need diffs will probably get a pretty good diff for the // first field and just the baseline diff for the second one. return d.strDiff(ctx, av, ev) } } switch av.Kind() { case reflect.Ptr, reflect.Interface: return d.diff(ctx, av.Elem(), ev.Elem()) case reflect.Slice, reflect.Array, reflect.String: if av.Len() != ev.Len() { // TODO: do a more thorough diff of values return d.genDiff(fmt.Sprintf("%s(len %%d)", av.Type()), av.Len(), ev.Len()) } var elems []string for i := 0; i < av.Len(); i++ { elems = append(elems, d.diff(ctx, av.Index(i), ev.Index(i))) } return "[ " + strings.Join(elems, ", ") + " ]" case reflect.Map: var parts []string for _, kv := range ev.MapKeys() { k := kv.Interface() bmv := ev.MapIndex(kv) amv := av.MapIndex(kv) if !amv.IsValid() { parts = append(parts, d.genDiff("%s", fmt.Sprintf("missing key %v", k), fmt.Sprintf("%v: %v", k, bmv.Interface()))) continue } parts = append(parts, fmt.Sprintf("%v: %s", k, d.diff(ctx, amv, bmv))) } for _, kv := range av.MapKeys() { // We've already compared all keys that exist in both maps; now we're // just looking for keys that only exist in a. if !ev.MapIndex(kv).IsValid() { k := kv.Interface() parts = append(parts, d.genDiff("%s", fmt.Sprintf("extra key %v: %v", k, av.MapIndex(kv).Interface()), fmt.Sprintf("%v: nil", k))) continue } } return "{" + strings.Join(parts, ", ") + "}" case reflect.Struct: if av.Type().Name() != ev.Type().Name() { return d.genDiff("%s", av.Type().Name(), ev.Type().Name()) + "(mismatched types)" } var parts []string for i := 0; i < ev.NumField(); i++ { f := ev.Type().Field(i) if f.PkgPath != "" && !f.Anonymous { // unexported continue } name := f.Name bfv := ev.Field(i) afv := av.Field(i) parts = append(parts, fmt.Sprintf("%s: %s", name, d.diff(ctx, afv, bfv))) } return fmt.Sprintf("%s{%s}", av.Type(), strings.Join(parts, ", ")) default: if av.Type().Comparable() { a, b := av.Interface(), ev.Interface() if a != b { return d.genDiff("%#v", a, b) } return fmt.Sprintf("%#v", a) } return d.format(fmt.Sprintf("UNSUPPORTED: could not compare values of type %s", av.Type())) } } // strDiff helps us generate a diff between two strings. It uses the baseStrAlgo // first to get a baseline, then uses results from the other algorithms set in // d.stringAlgos to get the lowest cost possible before returning. func (d *Differ) strDiff(ctx context.Context, av, ev reflect.Value) string { runeTyp := reflect.TypeOf([]rune(nil)) actual := av.Convert(runeTyp).Interface().([]rune) expected := ev.Convert(runeTyp).Interface().([]rune) var wg sync.WaitGroup results := make(chan str.Diff) for _, algo := range d.stringAlgos { algoCh := algo.Diffs(ctx, actual, expected) wg.Add(1) go func() { defer wg.Done() for { select { case diff, ok := <-algoCh: if !ok { return } results <- diff case <-ctx.Done(): return } } }() } go func() { // All of our algorithms are sending results to the same channel. Once // they are all done (either from the timeout or from exhausting all // options), we need to close the results channel to let the main logic // know that everything's done - we don't want to continue waiting if // all algorithms have exhausted their possible diffs. // // Since we know that the results channel will be closed once the // context times out, there's no reason to select on the results // channel. defer close(results) wg.Wait() }() best := baseStringDiff(actual, expected) for diff := range results { if diff.Cost() >= best.Cost() { continue } best = diff } var out string for _, section := range best.Sections() { if section.Type == str.TypeMatch { out += string(section.Actual) continue } out += d.genDiff("%s", string(section.Actual), string(section.Expected)) } return out } onpar-0.3.3/diff/diff_test.go000066400000000000000000000276211457413705600160660ustar00rootroot00000000000000package diff_test import ( "fmt" "runtime/debug" "strings" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/fatih/color" "github.com/poy/onpar" "github.com/poy/onpar/diff" "github.com/poy/onpar/diff/str" "github.com/poy/onpar/expect" "github.com/poy/onpar/matchers" ) const testTimeout = 5 * time.Second type testNestedStruct struct { testStruct T testStruct } type testStruct struct { Foo string Bar int // unexported is not explicitly tested, but if our code attempts to compare // unexported fields, it will panic. This is here to ensure that we notice // that. unexported *testStruct } func TestDiff(t *testing.T) { o := onpar.New(t) defer o.Run() for _, tt := range []struct { name string value any }{ {"string", "this is a string"}, {"int", 21}, {"slice", []string{"this", "is", "a", "slice", "of", "strings"}}, {"map", map[string]string{"maps": "should", "work": "too"}}, {"struct", testStruct{Foo: "foo", Bar: 42}}, {"pointer", &testStruct{Foo: "foo", Bar: 42}}, {"nested structs", testNestedStruct{testStruct: testStruct{Foo: "foo", Bar: 42}, T: testStruct{Foo: "baz", Bar: 42069}}}, } { tt := tt o.Spec(fmt.Sprintf("it does not call option functions for matching %s types", tt.name), func(t *testing.T) { out := diff.New(diff.WithFormat("!!!FAIL!!!%s!!!FAIL!!!")).Diff(tt.value, tt.value) if strings.Contains(out, "!!!FAIL!!!") { t.Fatalf("expected matching output to return without formatting; got %s", out) } }) } o.Spec("it doesn't care if pointer values are different", func(t *testing.T) { out := diff.New(diff.WithFormat("!!!FAIL!!!%s!!!FAIL!!!")).Diff(&testStruct{}, &testStruct{}) if strings.Contains(out, "!!!FAIL!!!") { t.Fatalf("expected different pointer values to recursively compare") } }) o.Spec("it shows the value of pointers when compared against nil", func(t *testing.T) { a := "foo" out := diff.New().Diff(&a, nil) if !strings.Contains(out, "foo") { t.Fatalf("output %s should have contained actual value '%s'", out, a) } out = diff.New().Diff(nil, &a) if !strings.Contains(out, "foo") { t.Fatalf("output %s should have contained expected value '%s'", out, a) } }) o.Spec("it can handle nil values", func(t *testing.T) { defer func() { if r := recover(); r != nil { t.Fatalf("nil values panicked (*diff.Differ).Diff: %v\n%s", r, string(debug.Stack())) } }() diff.New().Diff(nil, nil) diff.New().Diff(nil, 42) diff.New().Diff(42, nil) }) o.Spec("it diffs maps", func(t *testing.T) { a := map[string]string{"foo": "bar", "baz": "bazinga"} b := map[string]string{"foo": "baz", "bazinga": "baz"} expectedSubstrs := []string{"foo: ba>r!=z<", ">missing key bazinga!=bazinga: baz<", ">extra key baz: bazinga!=baz: nil<"} out := diff.New().Diff(a, b) for _, s := range expectedSubstrs { if !strings.Contains(out, s) { t.Fatalf("expected substring '%s' to exist in '%s'", s, out) } } }) for _, tt := range []struct { name string actual, expected any output string }{ {"different strings", "foo", "bar", ">foo!=bar<"}, {"different ints", 12, 14, ">12!=14<"}, {"different substrings", "foobarbaz", "fooeggbaz", "foo>bar!=eggr!=zingazinga!=rpyth!=gn!=eggs!=barcon!=z<"}, {"different struct fields", testStruct{Foo: "foo", Bar: 42}, testStruct{Foo: "bar", Bar: 42}, "diff_test.testStruct{Foo: >foo!=bar<, Bar: 42}"}, {"different anonymous fields", testNestedStruct{testStruct: testStruct{Foo: "foo", Bar: 42}, T: testStruct{Foo: "baz", Bar: 42069}}, testNestedStruct{testStruct: testStruct{Foo: "bar", Bar: 42}, T: testStruct{Foo: "baz", Bar: 42069}}, "diff_test.testNestedStruct{testStruct: diff_test.testStruct{Foo: >foo!=bar<, Bar: 42}, T: diff_test.testStruct{Foo: baz, Bar: 42069}}"}, {"different nested fields", testNestedStruct{testStruct: testStruct{Foo: "foo", Bar: 42}, T: testStruct{Foo: "baz", Bar: 42069}}, testNestedStruct{testStruct: testStruct{Foo: "foo", Bar: 42}, T: testStruct{Foo: "bazinga", Bar: 42069}}, "diff_test.testNestedStruct{testStruct: diff_test.testStruct{Foo: foo, Bar: 42}, T: diff_test.testStruct{Foo: baz>!=inga<, Bar: 42069}}"}, } { tt := tt o.Spec(fmt.Sprintf("it shows diffs for %s", tt.name), func(t *testing.T) { out := diff.New().Diff(tt.actual, tt.expected) if out != tt.output { t.Fatalf("expected the diff between %v and %v to be %s; got %s", tt.actual, tt.expected, tt.output, out) } }) } o.Spec("it calls Sprinters", func(t *testing.T) { s := newMockSprinter(t, time.Second) pers.Return(s.SprintOutput, "foo") out := diff.New(diff.WithSprinter(s)).Diff("first", "second") expect.Expect(t, s).To(pers.HaveMethodExecuted("Sprint", pers.WithArgs("firstsecond"))) expect.Expect(t, out).To(matchers.Equal("foo")) }) o.Spec("it does not hang on strings mentioned in issue 30", func(t *testing.T) { done := make(chan struct{}) go func() { defer close(done) diff.New().Diff( `{"current":[{"kind":0,"at":{"seconds":1596288863,"nanos":13000000},"msg":"Something bad happened."}]}`, `{"current": [{"kind": "GENERIC", "at": "2020-08-01T13:34:23.013Z", "msg": "Something bad happened."}], "history": []}`, ) }() select { case <-done: // This diff is not "stable" - with concurrent differs (like the // str.CharDiff differ that we're exercising), we can't guarantee // the same output every time. case <-time.After(testTimeout): t.Fatalf("timed out waiting for diff to finish") } }) o.Spec("it always includes a basic string diff", func(t *testing.T) { out := diff.New(diff.WithStringAlgos()).Diff("foobarbaz", "foobaconbaz") expect.Expect(t, out).To(matchers.Equal(">foobarbaz!=foobaconbaz<")) }) o.Group("with custom string algorithms", func() { type testCtx struct { t *testing.T algo1, algo2 *mockStringDiffAlgorithm timeout time.Duration differ *diff.Differ } o := onpar.BeforeEach(o, func(t *testing.T) testCtx { algo1 := newMockStringDiffAlgorithm(t, time.Second) algo2 := newMockStringDiffAlgorithm(t, time.Second) timeout := time.Second return testCtx{ t: t, algo1: algo1, algo2: algo2, timeout: timeout, differ: diff.New(diff.WithStringAlgos(algo1, algo2), diff.WithTimeout(timeout)), } }) o.Spec("it returns the basic diff when no better diffs are returned", func(tt testCtx) { ch := make(chan str.Diff) close(ch) pers.Return(tt.algo1.DiffsOutput, ch) pers.Return(tt.algo2.DiffsOutput, ch) out := tt.differ.Diff("foobar", "foobaz") expect.Expect(tt.t, out).To(matchers.Equal(">foobar!=foobaz<")) }) o.Spec("it returns a better diff from an algorithm", func(tt testCtx) { ch := make(chan str.Diff, 1) diff := newMockDiff(tt.t, time.Second) ch <- diff close(ch) pers.Return(tt.algo1.DiffsOutput, ch) pers.Return(tt.algo2.DiffsOutput, ch) pers.Return(diff.CostOutput, 2) pers.Return(diff.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("foob"), Expected: []rune("foob")}, {Type: str.TypeReplace, Actual: []rune("ar"), Expected: []rune("az")}, }) out := tt.differ.Diff("foobar", "foobaz") expect.Expect(tt.t, out).To(matchers.Equal("foob>ar!=az<")) }) o.Spec("it chooses the best diff regardless of order", func(tt testCtx) { ch := make(chan str.Diff, 2) diff1 := newMockDiff(tt.t, time.Second) diff2 := newMockDiff(tt.t, time.Second) ch <- diff1 ch <- diff2 close(ch) pers.Return(tt.algo1.DiffsOutput, ch) pers.Return(tt.algo2.DiffsOutput, ch) pers.ConsistentlyReturn(tt.t, diff1.CostOutput, 2) pers.ConsistentlyReturn(tt.t, diff1.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("foob"), Expected: []rune("foob")}, {Type: str.TypeReplace, Actual: []rune("ar"), Expected: []rune("az")}, }) pers.ConsistentlyReturn(tt.t, diff2.CostOutput, 5) pers.ConsistentlyReturn(tt.t, diff2.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("f"), Expected: []rune("f")}, {Type: str.TypeReplace, Actual: []rune("oobar"), Expected: []rune("oobaz")}, }) out := tt.differ.Diff("foobar", "foobaz") expect.Expect(tt.t, out).To(matchers.Equal("foob>ar!=az<")) }) o.Spec("it respects the timeout", func(tt testCtx) { ch := make(chan str.Diff, 1) diff := newMockDiff(tt.t, time.Second) ch <- diff pers.Return(tt.algo1.DiffsOutput, ch) pers.Return(tt.algo2.DiffsOutput, ch) pers.Return(diff.CostOutput, 2) pers.Return(diff.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("foob"), Expected: []rune("foob")}, {Type: str.TypeReplace, Actual: []rune("ar"), Expected: []rune("az")}, }) stop := make(chan struct{}) defer close(stop) out := make(chan string) go func() { defer close(out) select { case <-stop: case out <- tt.differ.Diff("foobar", "foobaz"): } }() gracePeriod := 10 * time.Millisecond timeout := time.After(tt.timeout + gracePeriod) select { case <-timeout: tt.t.Fatalf("did not receive output within %v", tt.timeout) case out := <-out: expect.Expect(tt.t, out).To(matchers.Equal("foob>ar!=az<")) } }) o.Spec("it runs algorithms concurrently and returns the lowest cost available", func(tt testCtx) { ch1 := make(chan str.Diff, 2) ch2 := make(chan str.Diff, 1) diff1 := newMockDiff(tt.t, time.Second) diff2 := newMockDiff(tt.t, time.Second) diff3 := newMockDiff(tt.t, time.Second) ch1 <- diff1 ch2 <- diff2 ch1 <- diff3 pers.Return(tt.algo1.DiffsOutput, ch1) pers.Return(tt.algo2.DiffsOutput, ch2) pers.ConsistentlyReturn(tt.t, diff1.CostOutput, 4) pers.Return(diff1.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("fo"), Expected: []rune("fo")}, {Type: str.TypeReplace, Actual: []rune("obar"), Expected: []rune("obaz")}, }) pers.ConsistentlyReturn(tt.t, diff2.CostOutput, 1) pers.Return(diff2.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("fooba"), Expected: []rune("fooba")}, {Type: str.TypeReplace, Actual: []rune("r"), Expected: []rune("z")}, }) pers.ConsistentlyReturn(tt.t, diff3.CostOutput, 2) pers.Return(diff3.SectionsOutput, []str.DiffSection{ {Type: str.TypeMatch, Actual: []rune("foob"), Expected: []rune("foob")}, {Type: str.TypeReplace, Actual: []rune("ar"), Expected: []rune("az")}, }) out := tt.differ.Diff("foobar", "foobaz") expect.Expect(tt.t, out).To(matchers.Equal("fooba>r!=z<")) }) }) } func ExampleDiffer_Diff() { fmt.Println(diff.New().Diff("some string with foo in it", "some string with bar in it")) // Output: // some string with >foo!=bar< in it } func ExampleWithFormat() { style := diff.WithFormat("LOOK-->%s<--!!!") fmt.Println(diff.New(style).Diff("some string with foo in it", "some string with bar in it")) // Output: // some string with LOOK-->foobar<--!!! in it } func ExampleActual() { styles := []diff.Opt{ diff.Actual( // styles passed to this will only apply to actual values diff.WithFormat("(%s|"), ), diff.Expected( // styles passed to this will only apply to expected values diff.WithFormat("%s)"), ), } fmt.Println(diff.New(styles...).Diff("some string with foo in it", "some string with bar in it")) // Output: // some string with (foo|bar) in it } func ExampleWithSprinter_color() { // WithSprinter is provided for integration with any type // that has an `Sprint(...any) string` method. Here // we use github.com/fatih/color. styles := []diff.Opt{ diff.Actual(diff.WithSprinter(color.New(color.CrossedOut, color.FgRed))), diff.Expected(diff.WithSprinter(color.New(color.FgYellow))), } fmt.Println(diff.New(styles...).Diff("some string with foo in it", "some string with bar in it")) } onpar-0.3.3/diff/doc.go000066400000000000000000000000401457413705600146460ustar00rootroot00000000000000//go:generate hel package diff onpar-0.3.3/diff/helheim_test.go000066400000000000000000000110161457413705600165600ustar00rootroot00000000000000// Code generated by git.sr.ht/~nelsam/hel. DO NOT EDIT. // // This file contains mocks generated by hel. Do not edit this code by // hand unless you *really* know what you're doing. Expect any changes // made manually to be overwritten the next time hel regenerates this // file. package diff_test import ( "context" "time" "git.sr.ht/~nelsam/hel/vegr" "github.com/poy/onpar/diff/str" ) type mockStringDiffAlgorithm struct { t vegr.T timeout time.Duration DiffsCalled chan bool DiffsInput struct { Ctx chan context.Context Actual, Expected chan []rune } DiffsOutput struct { Ret0 chan (<-chan str.Diff) } } func newMockStringDiffAlgorithm(t vegr.T, timeout time.Duration) *mockStringDiffAlgorithm { m := &mockStringDiffAlgorithm{t: t, timeout: timeout} m.DiffsCalled = make(chan bool, 100) m.DiffsInput.Ctx = make(chan context.Context, 100) m.DiffsInput.Actual = make(chan []rune, 100) m.DiffsInput.Expected = make(chan []rune, 100) m.DiffsOutput.Ret0 = make(chan (<-chan str.Diff), 100) return m } func (m *mockStringDiffAlgorithm) Diffs(ctx context.Context, actual, expected []rune) (ret0 <-chan str.Diff) { m.t.Helper() m.DiffsCalled <- true m.DiffsInput.Ctx <- ctx m.DiffsInput.Actual <- actual m.DiffsInput.Expected <- expected vegr.PopulateReturns(m.t, "Diffs", m.timeout, m.DiffsOutput, &ret0) return ret0 } type mockSprinter struct { t vegr.T timeout time.Duration SprintCalled chan bool SprintInput struct { Arg0 chan []any } SprintOutput struct { Ret0 chan string } } func newMockSprinter(t vegr.T, timeout time.Duration) *mockSprinter { m := &mockSprinter{t: t, timeout: timeout} m.SprintCalled = make(chan bool, 100) m.SprintInput.Arg0 = make(chan []any, 100) m.SprintOutput.Ret0 = make(chan string, 100) return m } func (m *mockSprinter) Sprint(arg0 ...any) (ret0 string) { m.t.Helper() m.SprintCalled <- true m.SprintInput.Arg0 <- arg0 vegr.PopulateReturns(m.t, "Sprint", m.timeout, m.SprintOutput, &ret0) return ret0 } type mockContext struct { t vegr.T timeout time.Duration DeadlineCalled chan bool DeadlineOutput struct { Deadline chan time.Time Ok chan bool } DoneCalled chan bool DoneOutput struct { Ret0 chan (<-chan struct{}) } ErrCalled chan bool ErrOutput struct { Ret0 chan error } ValueCalled chan bool ValueInput struct { Key chan any } ValueOutput struct { Ret0 chan any } } func newMockContext(t vegr.T, timeout time.Duration) *mockContext { m := &mockContext{t: t, timeout: timeout} m.DeadlineCalled = make(chan bool, 100) m.DeadlineOutput.Deadline = make(chan time.Time, 100) m.DeadlineOutput.Ok = make(chan bool, 100) m.DoneCalled = make(chan bool, 100) m.DoneOutput.Ret0 = make(chan (<-chan struct{}), 100) m.ErrCalled = make(chan bool, 100) m.ErrOutput.Ret0 = make(chan error, 100) m.ValueCalled = make(chan bool, 100) m.ValueInput.Key = make(chan any, 100) m.ValueOutput.Ret0 = make(chan any, 100) return m } func (m *mockContext) Deadline() (deadline time.Time, ok bool) { m.t.Helper() m.DeadlineCalled <- true vegr.PopulateReturns(m.t, "Deadline", m.timeout, m.DeadlineOutput, &deadline, &ok) return deadline, ok } func (m *mockContext) Done() (ret0 <-chan struct{}) { m.t.Helper() m.DoneCalled <- true vegr.PopulateReturns(m.t, "Done", m.timeout, m.DoneOutput, &ret0) return ret0 } func (m *mockContext) Err() (ret0 error) { m.t.Helper() m.ErrCalled <- true vegr.PopulateReturns(m.t, "Err", m.timeout, m.ErrOutput, &ret0) return ret0 } func (m *mockContext) Value(key any) (ret0 any) { m.t.Helper() m.ValueCalled <- true m.ValueInput.Key <- key vegr.PopulateReturns(m.t, "Value", m.timeout, m.ValueOutput, &ret0) return ret0 } type mockDiff struct { t vegr.T timeout time.Duration CostCalled chan bool CostOutput struct { Ret0 chan float64 } SectionsCalled chan bool SectionsOutput struct { Ret0 chan []str.DiffSection } } func newMockDiff(t vegr.T, timeout time.Duration) *mockDiff { m := &mockDiff{t: t, timeout: timeout} m.CostCalled = make(chan bool, 100) m.CostOutput.Ret0 = make(chan float64, 100) m.SectionsCalled = make(chan bool, 100) m.SectionsOutput.Ret0 = make(chan []str.DiffSection, 100) return m } func (m *mockDiff) Cost() (ret0 float64) { m.t.Helper() m.CostCalled <- true vegr.PopulateReturns(m.t, "Cost", m.timeout, m.CostOutput, &ret0) return ret0 } func (m *mockDiff) Sections() (ret0 []str.DiffSection) { m.t.Helper() m.SectionsCalled <- true vegr.PopulateReturns(m.t, "Sections", m.timeout, m.SectionsOutput, &ret0) return ret0 } onpar-0.3.3/diff/str/000077500000000000000000000000001457413705600143705ustar00rootroot00000000000000onpar-0.3.3/diff/str/char.go000066400000000000000000000153741457413705600156460ustar00rootroot00000000000000package str import ( "context" "sync" ) func greaterBaseCost(a, b []rune) float64 { if len(a) > len(b) { return float64(len(a)) } return float64(len(b)) } type charDiff struct { baseCost float64 perCharCost float64 cost float64 sections []DiffSection } func (d *charDiff) calculate() { d.cost = 0 for _, s := range d.sections { if s.Type == TypeMatch { continue } d.cost += d.baseCost + d.perCharCost*greaterBaseCost(s.Actual, s.Expected) } } func (d *charDiff) Cost() float64 { return d.cost } func (d *charDiff) Sections() []DiffSection { return d.sections } // broadcast is a type which can broadcast new diffs to multiple subscribers. type broadcast struct { mu sync.Mutex closed bool curr Diff subs []chan Diff } // subscribe subscribes to an existing broadcast, returning a channel to listen // for changes on. The current value will be sent on the channel immediately. func (b *broadcast) subscribe() chan Diff { ch := make(chan Diff, 1) b.mu.Lock() defer b.mu.Unlock() b.subs = append(b.subs, ch) if b.curr != nil { ch <- b.curr } if b.closed { close(ch) } return ch } // send sends d to all subscribers and updates the current value for new // subscribers. func (b *broadcast) send(ctx context.Context, d Diff) { b.mu.Lock() defer b.mu.Unlock() b.curr = d for _, s := range b.subs { select { case s <- d: case <-ctx.Done(): return } } } // done signals that b has exhausted all possibilities and all subscribers // should be closed. func (b *broadcast) done() { b.mu.Lock() defer b.mu.Unlock() b.closed = true for _, s := range b.subs { close(s) } } type diffIdx struct { aStart, eStart int } // CharDiffOpt is an option function for changing the behavior of the // NewCharDiff constructor. type CharDiffOpt func(CharDiff) CharDiff // CharDiffBaseCost is a CharDiff option to set the base cost per diff section. // Increasing this will reduce the number of diff sections in the output at the // cost of larger diff sections. // // Default is 0. func CharDiffBaseCost(cost float64) CharDiffOpt { return func(d CharDiff) CharDiff { d.baseCost = cost return d } } // CharDiffPerCharCost is a CharDiff option to set the cost-per-character of any // differences returned. Increasing this cost will reduce the size of diff // sections at the cost of more diff sections. // // Default is 1 func CharDiffPerCharCost(cost float64) CharDiffOpt { return func(d CharDiff) CharDiff { d.perCharCost = cost return d } } // CharDiff is a per-character diff algorithm, meaning that it makes no distinctions // about word or line boundaries when generating a diff. type CharDiff struct { baseCost float64 perCharCost float64 } func NewCharDiff(opts ...CharDiffOpt) *CharDiff { d := CharDiff{ baseCost: 0, perCharCost: 1, } for _, o := range opts { d = o(d) } return &d } func (c *CharDiff) Diffs(ctx context.Context, actual, expected []rune) <-chan Diff { ch := make(chan Diff) var m sync.Map go c.sendDiffs(ctx, ch, &m, actual, expected, 0, 0) return ch } func (c *CharDiff) sendBestResults(ctx context.Context, ch chan<- Diff, bcast *broadcast, baseSections []DiffSection) { defer close(ch) subCh := bcast.subscribe() var cheapest *charDiff for { select { case subDiff, ok := <-subCh: if !ok { return } diff := &charDiff{ baseCost: c.baseCost, perCharCost: c.perCharCost, sections: append([]DiffSection(nil), baseSections...), } diff.sections = append(diff.sections, subDiff.Sections()...) diff.calculate() if cheapest == nil || cheapest.Cost() > diff.Cost() { cheapest = diff select { case ch <- cheapest: case <-ctx.Done(): return } } case <-ctx.Done(): return } } } func (c *CharDiff) runBroadcast(ctx context.Context, bcast *broadcast, ch <-chan Diff, actual, expected []rune, actualStart, expectedStart int) { defer bcast.done() base := &charDiff{ baseCost: c.baseCost, perCharCost: c.perCharCost, sections: []DiffSection{ {Type: TypeReplace, Actual: actual[actualStart:], Expected: expected[expectedStart:]}, }, } base.calculate() shortest := Diff(base) bcast.send(ctx, shortest) if ctx.Err() != nil { return } for diff := range ch { if ctx.Err() != nil { return } if diff.Cost() >= shortest.Cost() { continue } shortest = diff bcast.send(ctx, shortest) } } func (c *CharDiff) sendSubDiffs(ctx context.Context, wg *sync.WaitGroup, subCh <-chan Diff, results chan<- Diff, section DiffSection) { defer wg.Done() for { select { case subDiff, ok := <-subCh: if !ok { return } diff := &charDiff{ baseCost: c.baseCost, perCharCost: c.perCharCost, sections: append([]DiffSection{section}, subDiff.Sections()...), } diff.calculate() select { case results <- diff: case <-ctx.Done(): return } case <-ctx.Done(): return } } } func (c *CharDiff) sendDiffs(ctx context.Context, ch chan<- Diff, cache *sync.Map, actual, expected []rune, actualStart, expectedStart int) { actualEnd, expectedEnd := actualStart, expectedStart for actualEnd < len(actual) && expectedEnd < len(expected) && actual[actualEnd] == expected[expectedEnd] { actualEnd++ expectedEnd++ } if actualEnd == len(actual) && expectedEnd == len(expected) { if actualEnd-actualStart > 0 || expectedEnd-expectedStart > 0 { diff := &charDiff{ baseCost: c.baseCost, perCharCost: c.perCharCost, sections: []DiffSection{{Type: TypeMatch, Actual: actual[actualStart:actualEnd], Expected: expected[expectedStart:expectedEnd]}}, } select { case ch <- diff: case <-ctx.Done(): } } close(ch) return } bcast := &broadcast{} cached, running := cache.LoadOrStore(diffIdx{aStart: actualEnd, eStart: expectedEnd}, bcast) bcast = cached.(*broadcast) var baseSections []DiffSection if actualEnd-actualStart > 0 || expectedEnd-expectedStart > 0 { baseSections = []DiffSection{ {Type: TypeMatch, Actual: actual[actualStart:actualEnd], Expected: expected[expectedStart:expectedEnd]}, } } go c.sendBestResults(ctx, ch, bcast, baseSections) if running { return } subCh := make(chan Diff) go c.runBroadcast(ctx, bcast, subCh, actual, expected, actualEnd, expectedEnd) var wg sync.WaitGroup for i := actualEnd; i < len(actual); i++ { for j := expectedEnd; j < len(expected); j++ { if ctx.Err() != nil { return } if actual[i] != expected[j] { continue } subSubCh := make(chan Diff) wg.Add(1) go c.sendSubDiffs(ctx, &wg, subSubCh, subCh, DiffSection{ Type: TypeReplace, Actual: actual[actualEnd:i], Expected: expected[expectedEnd:j], }) c.sendDiffs(ctx, subSubCh, cache, actual, expected, i, j) } } go closeAfter(subCh, &wg) } func closeAfter(ch chan<- Diff, wg *sync.WaitGroup) { wg.Wait() close(ch) } onpar-0.3.3/diff/str/char_test.go000066400000000000000000000151651457413705600167030ustar00rootroot00000000000000package str_test import ( "context" "fmt" "math" "reflect" "strings" "testing" "time" "github.com/poy/onpar" "github.com/poy/onpar/diff/str" "github.com/poy/onpar/expect" "github.com/poy/onpar/matchers" ) func TestCharDiff(t *testing.T) { o := onpar.New(t) defer o.Run() matchReplace := func(t *testing.T, start str.Type, l ...string) []str.DiffSection { // matchReplace is used to generate match/replace cadence for expected // values, since by default the char diff will always return diffs that // have a match followed by a replace followed by a match, and so on. t.Helper() var sections []str.DiffSection curr := start for _, v := range l { section := str.DiffSection{ Type: curr, Actual: []rune(v), Expected: []rune(v), } if curr == str.TypeReplace { // NOTE: if any of the diffs we need to test contain a pipe // character, this will break. parts := strings.Split(v, "|") if len(parts) != 2 { t.Fatalf("test error: expected replace string (%v) to use a | character to separate actual and expected values, but got %d values when splitting", v, len(parts)) } section.Actual = []rune(parts[0]) section.Expected = []rune(parts[1]) } sections = append(sections, section) curr = 1 - curr } return sections } replace := func(t *testing.T, a, b string) string { // replace is used to generate replacement strings for the matchReplace // function. This helps make the test read more clearly, while also // checking for separator characters in the source strings. t.Helper() if strings.Contains(a, "|") { t.Fatalf("replace source string %v contains pipe character", a) } if strings.Contains(b, "|") { t.Fatalf("replace source string %v contains pipe character", b) } return fmt.Sprintf("%s|%s", a, b) } o.Group("exhaustive results", func() { finalDiff := func(t *testing.T, timeout time.Duration, diffs <-chan str.Diff) str.Diff { t.Helper() var final str.Diff tCh := time.After(timeout) for { select { case next, ok := <-diffs: if !ok { return final } final = next case <-tCh: t.Fatalf("failed to exhaust results within %v", timeout) } } } for _, tt := range []struct { name string actual, expected string output []str.DiffSection }{ {"different strings", "foo", "bar", []str.DiffSection{{Type: str.TypeReplace, Actual: []rune("foo"), Expected: []rune("bar")}}}, {"different substrings", "foobarbaz", "fooeggbaz", matchReplace(t, str.TypeMatch, "foo", replace(t, "bar", "egg"), "baz")}, {"longer expected string", "foobarbaz", "foobazingabaz", matchReplace(t, str.TypeMatch, "fooba", replace(t, "r", "zinga"), "baz")}, {"longer actual string", "foobazingabaz", "foobarbaz", matchReplace(t, str.TypeMatch, "fooba", replace(t, "zinga", "r"), "baz")}, {"multiple different substrings", "pythonfooeggsbazingabacon", "gofoobarbazingabaz", matchReplace(t, str.TypeReplace, replace(t, "pyth", "g"), "o", replace(t, "n", ""), "foo", replace(t, "eggs", "bar"), "bazingaba", replace(t, "con", "z"), ), }, } { tt := tt o.Spec(tt.name, func(t *testing.T) { ch := str.NewCharDiff().Diffs(context.Background(), []rune(tt.actual), []rune(tt.expected)) final := finalDiff(t, time.Second, ch) exp := readableSections(tt.output) for i, v := range readableSections(final.Sections()) { // NOTE: these matches will be checked again below; // this is just to provide more detail about which // indexes failed. We could use a differ, but since // this test is testing differs, a bug in the differ // might break the test (rather than simply failing // it). if i > len(exp) { t.Errorf("actual (length %d) was longer than expected (length %d)", len(final.Sections()), len(exp)) break } if !reflect.DeepEqual(v, exp[i]) { t.Errorf("%#v did not match %#v", v, exp[i]) } } expect.Expect(t, readableSections(final.Sections())).To(matchers.Equal(readableSections(tt.output))) }) } o.Spec("it doesn't hang on strings mentioned in issue 30", func(t *testing.T) { // This diff has multiple options for the "best" result (more than // one diff at the lowest possible cost). So we can't very well // assert on the exact diff returned, but we can assert on the cost // and the total actual and expected strings. actual := `{"current":[{"kind":0,"at":{"seconds":1596288863,"nanos":13000000},"msg":"Something bad happened."}]}` expected := `{"current": [{"kind": "GENERIC", "at": "2020-08-01T13:34:23.013Z", "msg": "Something bad happened."}], "history": []}` diffs := str.NewCharDiff().Diffs(context.Background(), []rune(actual), []rune(expected)) final := finalDiff(t, time.Second, diffs) expectedCost := float64(57) expect.Expect(t, final.Cost()).To(matchers.Equal(expectedCost)) var retActual, retExpected string for _, v := range final.Sections() { retActual += string(v.Actual) retExpected += string(v.Expected) } expect.Expect(t, retActual).To(matchers.Equal(actual)) expect.Expect(t, retExpected).To(matchers.Equal(expected)) }) }) o.Spec("it returns decreasingly costly results until the context is done", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() actual := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" expected := "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." diffs := str.NewCharDiff().Diffs(ctx, []rune(actual), []rune(expected)) // We want to check a few results, ensuring that the cost is lower each // time, before cancelling the context to ensure that the results stop // when the context finishes. const toCheck = 5 best := math.MaxFloat64 for i := 0; i < toCheck; i++ { v := <-diffs if v.Cost() > best { t.Fatalf("new cost (%f) is greater than old cost (%f)", v.Cost(), best) } best = v.Cost() } cancel() // Ensure that the logic has time to shut down before we resume reading // from the channel. time.Sleep(100 * time.Millisecond) if _, ok := <-diffs; ok { t.Fatalf("results channel is still open after cancelling the context") } }) } type readableSection struct { typ str.Type actual, expected string } func readableSections(s []str.DiffSection) []readableSection { var rs []readableSection for _, v := range s { rs = append(rs, readableSection{ typ: v.Type, actual: string(v.Actual), expected: string(v.Expected), }) } return rs } onpar-0.3.3/diff/str/diff.go000066400000000000000000000015451457413705600156340ustar00rootroot00000000000000package str type Type int const ( TypeMatch = iota TypeReplace ) // DiffSection represents a single chunk of a diff. type DiffSection struct { Type Type Actual, Expected []rune } // Diff represents a full diff of two values. type Diff interface { // Cost is the calculated cost of changing from one value to another. // Basically, if provided with multiple diffs, the Differ will always prefer // the lowest cost. // // Generally, a cost of 0 should represent exactly equal values, so negative // numbers shouldn't usually be used. However, if they are used, they will // work the same as positive values, being preferred over any value higher // than them. Cost() float64 // Sections returns all of the sections of the diff. This will be used to // generate output, depending on the diff formats being used. Sections() []DiffSection } onpar-0.3.3/expect/000077500000000000000000000000001457413705600141405ustar00rootroot00000000000000onpar-0.3.3/expect/README.md000066400000000000000000000007351457413705600154240ustar00rootroot00000000000000# expect `Expect` is used to used to invoke matchers and give a helpful error message. `Expect` helps a test read well and minimize overhead and scaffolding. ```go func TestSomething(t *testing.T) { x := "foo" Expect(t, x).To(Equal("bar")) } ``` The previous example will fail and pass the error from the matcher `Equal` to `t.Fatal`. ```go func TestSomething(t *testing.T) { x := "foo" Expect(t, x).To(StartsWith("foobar")) } ``` This example will pass. onpar-0.3.3/expect/expect.go000066400000000000000000000036751457413705600157720ustar00rootroot00000000000000package expect import ( "path" "runtime" "github.com/poy/onpar/matchers" ) // ToMatcher is a type that can be passed to (*To).To(). type ToMatcher interface { Match(actual any) (resultValue any, err error) } // Differ is a type of matcher that will need to diff its expected and // actual values. type DiffMatcher interface { UseDiffer(matchers.Differ) } // T is a type that we can perform assertions with. type T interface { Fatalf(format string, args ...any) } // THelper has the method that tells the testing framework that it can declare // itself a test helper. type THelper interface { Helper() } // Opt is an option that can be passed to New to modify Expectations. type Opt func(To) To // WithDiffer stores the diff.Differ to be used when displaying diffs between // actual and expected values. func WithDiffer(d matchers.Differ) Opt { return func(t To) To { t.differ = d return t } } // Expectation is provided to make it clear what the expect function does. type Expectation func(actual any) *To // New creates a new Expectation func New(t T, opts ...Opt) Expectation { return func(actual any) *To { to := To{ actual: actual, t: t, } for _, opt := range opts { to = opt(to) } return &to } } // Expect performs New(t)(actual). func Expect(t T, actual any) *To { return New(t)(actual) } // To is a type that stores actual values prior to running them through // matchers. type To struct { actual any parentErr error t T differ matchers.Differ } // To takes a matcher and passes it the actual value, failing t's T value // if the matcher returns an error. func (t *To) To(matcher matchers.Matcher) { if helper, ok := t.t.(THelper); ok { helper.Helper() } if d, ok := matcher.(DiffMatcher); ok { d.UseDiffer(t.differ) } _, err := matcher.Match(t.actual) if err != nil { _, fileName, lineNumber, _ := runtime.Caller(1) t.t.Fatalf("%s\n%s:%d", err.Error(), path.Base(fileName), lineNumber) } } onpar-0.3.3/expect/expect_test.go000066400000000000000000000027101457413705600170160ustar00rootroot00000000000000//go:generate hel package expect_test import ( "fmt" "reflect" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" . "github.com/poy/onpar/expect" ) type diffMatcher struct { mockToMatcher mockDiffMatcher } func TestToRespectsDifferMatchers(t *testing.T) { mockT := newMockT(t, time.Second) d := newMockDiffMatcher(t, time.Second) m := newMockToMatcher(t, time.Second) mockMatcher := &diffMatcher{ mockDiffMatcher: *d, mockToMatcher: *m, } pers.Return(mockMatcher.MatchOutput, nil, nil) mockDiffer := newMockDiffer(t, time.Second) f := New(mockT, WithDiffer(mockDiffer)) f(101).To(mockMatcher) Expect(t, mockMatcher).To(pers.HaveMethodExecuted("UseDiffer", pers.WithArgs(mockDiffer))) } func TestToPassesActualToMatcher(t *testing.T) { mockT := newMockT(t, time.Second) mockMatcher := newMockToMatcher(t, time.Second) pers.Return(mockMatcher.MatchOutput, nil, nil) Expect(mockT, 101).To(mockMatcher) select { case actual := <-mockMatcher.MatchInput.Actual: if !reflect.DeepEqual(actual, 101) { t.Errorf("Expected %v to equal %v", actual, 101) } default: t.Errorf("Expected Match() to be invoked") } } func TestToErrorsIfMatcherFails(t *testing.T) { mockT := newMockT(t, time.Second) mockMatcher := newMockToMatcher(t, time.Second) pers.Return(mockMatcher.MatchOutput, nil, fmt.Errorf("some-error")) Expect(mockT, 101).To(mockMatcher) if len(mockT.FatalfCalled) != 1 { t.Error("expected Fatalf to be invoked 1 time") } } onpar-0.3.3/expect/helheim_test.go000066400000000000000000000077471457413705600171600ustar00rootroot00000000000000// Code generated by git.sr.ht/~nelsam/hel. DO NOT EDIT. // // This file contains mocks generated by hel. Do not edit this code by // hand unless you *really* know what you're doing. Expect any changes // made manually to be overwritten the next time hel regenerates this // file. package expect_test import ( "time" "git.sr.ht/~nelsam/hel/vegr" "github.com/poy/onpar/matchers" ) type mockToMatcher struct { t vegr.T timeout time.Duration MatchCalled chan bool MatchInput struct { Actual chan any } MatchOutput struct { ResultValue chan any Err chan error } } func newMockToMatcher(t vegr.T, timeout time.Duration) *mockToMatcher { m := &mockToMatcher{t: t, timeout: timeout} m.MatchCalled = make(chan bool, 100) m.MatchInput.Actual = make(chan any, 100) m.MatchOutput.ResultValue = make(chan any, 100) m.MatchOutput.Err = make(chan error, 100) return m } func (m *mockToMatcher) Match(actual any) (resultValue any, err error) { m.t.Helper() m.MatchCalled <- true m.MatchInput.Actual <- actual vegr.PopulateReturns(m.t, "Match", m.timeout, m.MatchOutput, &resultValue, &err) return resultValue, err } type mockDiffMatcher struct { t vegr.T timeout time.Duration UseDifferCalled chan bool UseDifferInput struct { Arg0 chan matchers.Differ } } func newMockDiffMatcher(t vegr.T, timeout time.Duration) *mockDiffMatcher { m := &mockDiffMatcher{t: t, timeout: timeout} m.UseDifferCalled = make(chan bool, 100) m.UseDifferInput.Arg0 = make(chan matchers.Differ, 100) return m } func (m *mockDiffMatcher) UseDiffer(arg0 matchers.Differ) { m.t.Helper() m.UseDifferCalled <- true m.UseDifferInput.Arg0 <- arg0 } type mockT struct { t vegr.T timeout time.Duration FatalfCalled chan bool FatalfInput struct { Format chan string Args chan []any } } func newMockT(t vegr.T, timeout time.Duration) *mockT { m := &mockT{t: t, timeout: timeout} m.FatalfCalled = make(chan bool, 100) m.FatalfInput.Format = make(chan string, 100) m.FatalfInput.Args = make(chan []any, 100) return m } func (m *mockT) Fatalf(format string, args ...any) { m.t.Helper() m.FatalfCalled <- true m.FatalfInput.Format <- format m.FatalfInput.Args <- args } type mockTHelper struct { t vegr.T timeout time.Duration HelperCalled chan bool } func newMockTHelper(t vegr.T, timeout time.Duration) *mockTHelper { m := &mockTHelper{t: t, timeout: timeout} m.HelperCalled = make(chan bool, 100) return m } func (m *mockTHelper) Helper() { m.t.Helper() m.HelperCalled <- true } type mockDiffer struct { t vegr.T timeout time.Duration DiffCalled chan bool DiffInput struct { Actual, Expected chan any } DiffOutput struct { Ret0 chan string } } func newMockDiffer(t vegr.T, timeout time.Duration) *mockDiffer { m := &mockDiffer{t: t, timeout: timeout} m.DiffCalled = make(chan bool, 100) m.DiffInput.Actual = make(chan any, 100) m.DiffInput.Expected = make(chan any, 100) m.DiffOutput.Ret0 = make(chan string, 100) return m } func (m *mockDiffer) Diff(actual, expected any) (ret0 string) { m.t.Helper() m.DiffCalled <- true m.DiffInput.Actual <- actual m.DiffInput.Expected <- expected vegr.PopulateReturns(m.t, "Diff", m.timeout, m.DiffOutput, &ret0) return ret0 } type mockMatcher struct { t vegr.T timeout time.Duration MatchCalled chan bool MatchInput struct { Actual chan any } MatchOutput struct { ResultValue chan any Err chan error } } func newMockMatcher(t vegr.T, timeout time.Duration) *mockMatcher { m := &mockMatcher{t: t, timeout: timeout} m.MatchCalled = make(chan bool, 100) m.MatchInput.Actual = make(chan any, 100) m.MatchOutput.ResultValue = make(chan any, 100) m.MatchOutput.Err = make(chan error, 100) return m } func (m *mockMatcher) Match(actual any) (resultValue any, err error) { m.t.Helper() m.MatchCalled <- true m.MatchInput.Actual <- actual vegr.PopulateReturns(m.t, "Match", m.timeout, m.MatchOutput, &resultValue, &err) return resultValue, err } onpar-0.3.3/go.mod000066400000000000000000000011571457413705600137620ustar00rootroot00000000000000module github.com/poy/onpar go 1.21.4 require ( git.sr.ht/~nelsam/hel v0.6.6 github.com/fatih/color v1.16.0 ) require ( git.sr.ht/~nelsam/correct v0.0.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect golang.org/x/sys v0.17.0 // indirect ) // go modules make anything above v0 extremely hard to support, so we're // scrapping any versions above v0. From now on, hel will follow v0.x.x // versioning. // // This retract directive retracts all releases beyond v0. retract [v1.0.0, v1.1.3] onpar-0.3.3/go.sum000066400000000000000000000027131457413705600140060ustar00rootroot00000000000000git.sr.ht/~nelsam/correct v0.0.5 h1:99VgixW0fQBEnr48+yQy4U4pfcTpI/4wIMDy1q+r00c= git.sr.ht/~nelsam/correct v0.0.5/go.mod h1:m/urdFD4XS5ULMnod9lCHbVKxVA7vU+uADq+BMZ3C2o= git.sr.ht/~nelsam/hel v0.6.6 h1:5AJQPKZa9Y6ThwkfvdlxJKjOPEjT7rYqBsh+F1PKISI= git.sr.ht/~nelsam/hel v0.6.6/go.mod h1:aAXF8r8V5vtry0o9Lk7jIJB5yW8RNqu9Hg/L5OilVF4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= onpar-0.3.3/golden_output_test.go000066400000000000000000000016321457413705600171300ustar00rootroot00000000000000//go:build goldenoutput package onpar_test import ( "testing" "github.com/poy/onpar" ) func TestNestedStructure(t *testing.T) { // This test is used to generate verbose output to ensure that nested // structure creates the expected nested calls to t.Run. We have some more // unit-like tests that assert on these things too, but they can only really // test that the test names use the expected paths. They cannot prove that // each group has its own t.Run. top := onpar.New(t) defer top.Run() top.Spec("foo", func(*testing.T) {}) b := onpar.BeforeEach(top, func(t *testing.T) *testing.T { return t }) b.Spec("bar", func(*testing.T) {}) b.Group("baz", func() { b := onpar.BeforeEach(b, func(t *testing.T) string { return "foo" }) b.Spec("foo", func(string) {}) b.Group("bar", func() { b.Spec("foo", func(string) {}) }) b.Group("", func() { b.Spec("baz", func(string) {}) }) }) } onpar-0.3.3/golden_test.go000066400000000000000000000050601457413705600155070ustar00rootroot00000000000000//go:build golden package onpar_test import ( "flag" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" ) const goldenDir = "testdata" var ( update = flag.Bool("update", false, "update the golden files") // timingRe is a regular expression to find timings in test output. timingRe = regexp.MustCompile(`[0-9]+\.[0-9]{2,}s`) ) func zeroTimings(line string) string { return timingRe.ReplaceAllString(line, "0.00s") } func goldenPath(path ...string) string { full := append([]string{goldenDir}, path...) return filepath.Join(full...) } func goldenFile(t *testing.T, path ...string) []byte { fullPath := goldenPath(path...) f, err := os.Open(fullPath) if err != nil { t.Fatalf("golden: could not open file %v for reading: %v", fullPath, err) } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { t.Fatalf("golden: could not read file %v: %v", fullPath, err) } return b } func updateGoldenFile(t *testing.T, body []byte, path ...string) { fullPath := goldenPath(path...) f, err := os.Create(fullPath) if err != nil { t.Fatalf("golden: could not open file %v for writing: %v", fullPath, err) } defer f.Close() for len(body) > 0 { n, err := f.Write(body) if err != nil { t.Fatalf("golden: could not write to file %v: %v", fullPath, err) } body = body[n:] } } func TestMain(m *testing.M) { flag.Parse() os.Exit(m.Run()) } func TestVerboseOutput(t *testing.T) { got, err := exec.Command("go", "test", "-v", "-tags", "goldenoutput", "-run", "TestNestedStructure").CombinedOutput() if err != nil { t.Fatalf("golden: tests failed: %v", err) } fn := "verbose.out" if *update { updateGoldenFile(t, got, fn) return } // All tests run in parallel, so we can't rely on the lines coming out in // the same order. But we _can_ test that all lines exist in the output, // complete with matching indentation. // // This is mainly to prove that t.Run calls are nested properly. want := goldenFile(t, fn) wantedLines := strings.Split(string(want), "\n") gotLines := strings.Split(string(got), "\n") if len(wantedLines) != len(gotLines) { t.Fatalf("expected %d lines of output; got %d", len(wantedLines), len(gotLines)) } for _, wl := range wantedLines { wl = zeroTimings(wl) found := false for i, gl := range gotLines { gl = zeroTimings(gl) if wl == gl { gotLines = append(gotLines[:i], gotLines[i+1:]...) found = true break } } if !found { t.Errorf("missing line %v in actual output", wl) } } for _, l := range gotLines { t.Errorf("extra line %v in actual output", l) } } onpar-0.3.3/helheim_test.go000066400000000000000000000046541457413705600156620ustar00rootroot00000000000000// Code generated by git.sr.ht/~nelsam/hel. DO NOT EDIT. // // This file contains mocks generated by hel. Do not edit this code by // hand unless you *really* know what you're doing. Expect any changes // made manually to be overwritten the next time hel regenerates this // file. package onpar_test import ( "testing" "time" "git.sr.ht/~nelsam/hel/vegr" "git.sr.ht/~nelsam/hel/vegr/ret" ) var ( _ = vegr.EnforceVersion(6 - vegr.MinVersion) _ = vegr.EnforceVersion(vegr.MaxVersion - 6) ) type mockTestRunner struct { t vegr.T timeout time.Duration RunCalled chan bool RunInput struct { Name chan string Fn chan func(*testing.T) } RunOutput struct { MockReturnBlocker ret.Blocker MockPanicker ret.Panicker Ret0 chan bool } FailedCalled chan bool FailedOutput struct { MockReturnBlocker ret.Blocker MockPanicker ret.Panicker Ret0 chan bool } CleanupCalled chan bool CleanupInput struct { Arg0 chan func() } CleanupOutput struct { MockReturnBlocker ret.Blocker MockPanicker ret.Panicker } } func newMockTestRunner(t vegr.T, timeout time.Duration) *mockTestRunner { m := &mockTestRunner{t: t, timeout: timeout} m.RunCalled = make(chan bool, 100) m.RunInput.Name = make(chan string, 100) m.RunInput.Fn = make(chan func(*testing.T), 100) m.RunOutput.MockReturnBlocker = vegr.BlockChan() m.RunOutput.MockPanicker = make(ret.Panicker, 100) m.RunOutput.Ret0 = make(chan bool, 100) m.FailedCalled = make(chan bool, 100) m.FailedOutput.MockReturnBlocker = vegr.BlockChan() m.FailedOutput.MockPanicker = make(ret.Panicker, 100) m.FailedOutput.Ret0 = make(chan bool, 100) m.CleanupCalled = make(chan bool, 100) m.CleanupInput.Arg0 = make(chan func(), 100) m.CleanupOutput.MockReturnBlocker = vegr.BlockChan() m.CleanupOutput.MockPanicker = make(ret.Panicker, 100) return m } func (m *mockTestRunner) Run(name string, fn func(*testing.T)) (ret0 bool) { m.t.Helper() m.RunCalled <- true m.RunInput.Name <- name m.RunInput.Fn <- fn vegr.PopulateReturns(m.t, "Run", m.timeout, m.RunOutput, &ret0) return ret0 } func (m *mockTestRunner) Failed() (ret0 bool) { m.t.Helper() m.FailedCalled <- true vegr.PopulateReturns(m.t, "Failed", m.timeout, m.FailedOutput, &ret0) return ret0 } func (m *mockTestRunner) Cleanup(arg0 func()) { m.t.Helper() m.CleanupCalled <- true m.CleanupInput.Arg0 <- arg0 vegr.PopulateReturns(m.t, "Cleanup", m.timeout, m.CleanupOutput) } onpar-0.3.3/matchers/000077500000000000000000000000001457413705600144565ustar00rootroot00000000000000onpar-0.3.3/matchers/README.md000066400000000000000000000065311457413705600157420ustar00rootroot00000000000000# OnPar Matchers OnPar provides a set of minimalistic matchers to get you started. However, the intention is to be able to write your own custom matchers so that your code is more readable. ## Matchers List - [String Matchers](#string-matchers) - [Logical Matchers](#logical-matchers) - [Error Matchers](#error-matchers) - [Channel Matchers](#channel-matchers) - [Collection Matchers](#collection-matchers) - [Other Matchers](#other-matchers) ## String Matchers ### StartWith StartWithMatcher accepts a string and succeeds if the actual string starts with the expected string. ```go Expect(t, "foobar").To(StartWith("foo")) ``` ### EndWith EndWithMatcher accepts a string and succeeds if the actual string ends with the expected string. ```go Expect(t, "foobar").To(EndWith("bar")) ``` ### ContainSubstring ContainSubstringMatcher accepts a string and succeeds if the expected string is a sub-string of the actual. ```go Expect(t, "foobar").To(ContainSubstring("ooba")) ``` ### MatchRegexp ## Logical Matchers ### Not NotMatcher accepts a matcher and will succeed if the specified matcher fails. ```go Expect(t, false).To(Not(BeTrue())) ``` ### BeAbove BeAboveMatcher accepts a float64. It succeeds if the actual is greater than the expected. ```go Expect(t, 100).To(BeAbove(99)) ``` ### BeBelow BeBelowMatcher accepts a float64. It succeeds if the actual is less than the expected. ```go Expect(t, 100).To(BeBelow(101)) ``` ### BeFalse BeFalseMatcher will succeed if actual is false. ```go Expect(t, 2 == 3).To(BeFalse()) ``` ### BeTrue BeTrueMatcher will succeed if actual is true. ```go Expect(t, 2 == 2).To(BeTrue()) ``` ### Equal EqualMatcher performs a DeepEqual between the actual and expected. ```go Expect(t, 42).To(Equal(42)) ``` ## Error Matchers ### HaveOccurred HaveOccurredMatcher will succeed if the actual value is a non-nil error. ```go Expect(t, err).To(HaveOccurred()) Expect(t, nil).To(Not(HaveOccurred())) ``` ## Channel Matchers ### Receive ReceiveMatcher only accepts a readable channel. It will error for anything else. It will attempt to receive from the channel but will not block. It fails if the channel is closed. ```go c := make(chan bool, 1) c <- true Expect(t, c).To(Receive()) ``` ### BeClosed BeClosedMatcher only accepts a readable channel. It will error for anything else. It will succeed if the channel is closed. ```go c := make(chan bool) close(c) Expect(t, c).To(BeClosed()) ``` ## Collection Matchers ### HaveCap This matcher works on Slices, Arrays, Maps and Channels and will succeed if the type has the specified capacity. ```go Expect(t, []string{"foo", "bar"}).To(HaveCap(2)) ``` ### HaveKey HaveKeyMatcher accepts map types and will succeed if the map contains the specified key. ```go Expect(t, fooMap).To(HaveKey("foo")) ``` ### HaveLen HaveLenMatcher accepts Strings, Slices, Arrays, Maps and Channels. It will succeed if the type has the specified length. ```go Expect(t, "12345").To(HaveLen(5)) ``` ## Other Matchers ### Always AlwaysMatcher matches by polling the child matcher until it returns an error. It will return an error the first time the child matcher returns an error. If the child matcher never returns an error, then it will return a nil. By default, the duration is 100ms with an interval of 10ms. ```go isTrue := func() bool { return true } Expect(t, isTrue).To(Always(BeTrue())) ``` ### Chain ### ViaPolling onpar-0.3.3/matchers/always.go000066400000000000000000000030261457413705600163060ustar00rootroot00000000000000package matchers import ( "time" ) // AlwaysMatcher matches by polling the child matcher until it returns an error. // It will return an error the first time the child matcher returns an // error. If the child matcher never returns an error, // then it will return a nil. // // Duration is the longest scenario for the matcher // if the child matcher continues to return nil // // Interval is the period between polling. type AlwaysMatcher struct { Matcher Matcher Duration, Interval time.Duration } // Always returns a default AlwaysMatcher. Length of 100ms and rate 10ms func Always(m Matcher) AlwaysMatcher { return AlwaysMatcher{ Matcher: m, } } // Match takes a value that can change over time. Therefore, the only // two valid options are a function with no arguments and a single return // type, or a readable channel. Anything else will return an error. // // If actual is a channel, then the child matcher will have to handle // reading from the channel. // // If the actual is a function, then the matcher will invoke the value // and pass the returned value to the child matcher. func (m AlwaysMatcher) Match(actual any) (any, error) { if m.Duration == 0 { m.Duration = 100 * time.Millisecond } if m.Interval == 0 { m.Interval = 10 * time.Millisecond } f, err := fetchFunc(actual) if err != nil { return nil, err } var value any for i := 0; i < int(m.Duration/m.Interval); i++ { value, err = m.Matcher.Match(f()) if err != nil { return nil, err } time.Sleep(m.Interval) } return value, nil } onpar-0.3.3/matchers/always_test.go000066400000000000000000000033051457413705600173450ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "github.com/poy/onpar/matchers" ) func TestFailsWhenMatcherFails(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.Always(matcher) matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- fmt.Errorf("some-error") callCount := make(chan bool, 200) f := func() int { callCount <- true return 99 } _, err := m.Match(f) if err == nil { t.Error("expected err to not be nil") } if len(callCount) != 1 { t.Errorf("expected callCount (len=%d) to have a len of %d", len(callCount), 1) } } func TestPolls10TimesForSuccess(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.Always(matcher) i := 0 callCount := make(chan bool, 200) f := func() int { i++ callCount <- true matcher.MatchOutput.ResultValue <- i matcher.MatchOutput.Err <- nil return i } v, err := m.Match(f) if err != nil { t.Fatal("expected err to be nil") } if !reflect.DeepEqual(v, 10) { t.Errorf("expected %v to equal %v", v, 10) } if len(callCount) != 10 { t.Errorf("expected callCount (len=%d) to have a len of %d", len(callCount), 10) } } func TestPollsEach10msForSuccess(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.Always(matcher) var ts []int64 f := func() int { ts = append(ts, time.Now().UnixNano()) matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- nil return 101 } m.Match(f) for i := 0; i < len(ts)-1; i++ { if ts[i+1]-ts[i] < int64(10*time.Millisecond) || ts[i+1]-ts[i] > int64(30*time.Millisecond) { t.Fatalf("expected %d to be within 10ms and 30ms", ts[i+1]-ts[i]) } } } onpar-0.3.3/matchers/and.go000066400000000000000000000006141457413705600155500ustar00rootroot00000000000000package matchers type AndMatcher struct { Children []Matcher } func And(a, b Matcher, ms ...Matcher) AndMatcher { return AndMatcher{ Children: append(append([]Matcher{a}, b), ms...), } } func (m AndMatcher) Match(actual any) (any, error) { var err error for _, child := range m.Children { _, err = child.Match(actual) if err != nil { return nil, err } } return actual, nil } onpar-0.3.3/matchers/and_test.go000066400000000000000000000035601457413705600166120ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/poy/onpar/matchers" ) func TestAndSuccessUsesEachMatcher(t *testing.T) { mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, nil) pers.Return(mockMatcherB.MatchOutput, 2, nil) pers.Return(mockMatcherC.MatchOutput, 3, nil) m := matchers.And(mockMatcherA, mockMatcherB, mockMatcherC) v, err := m.Match(101) if err != nil { t.Error("expected err to be nil") } actual := <-mockMatcherA.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } actual = <-mockMatcherB.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } actual = <-mockMatcherC.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } if !reflect.DeepEqual(v, 101) { t.Errorf("expecte %v to equal %v", v, 101) } } func TestAndStopsOnFailure(t *testing.T) { mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, nil) pers.Return(mockMatcherB.MatchOutput, 2, fmt.Errorf("some-error")) pers.Return(mockMatcherC.MatchOutput, 3, nil) m := matchers.And(mockMatcherA, mockMatcherB, mockMatcherC) _, err := m.Match(101) if err == nil { t.Error("expected err to not be nil") } if len(mockMatcherA.MatchCalled) != 1 { t.Errorf("expected Match() to be invoked 1 time") } if len(mockMatcherB.MatchCalled) != 1 { t.Errorf("expected Match() to be invoked 1 time") } if len(mockMatcherC.MatchCalled) != 0 { t.Errorf("expected Match() to be invoked 0 times") } } onpar-0.3.3/matchers/be_above.go000066400000000000000000000016341457413705600165530ustar00rootroot00000000000000package matchers import "fmt" // BeAboveMatcher accepts a float64. It succeeds if the // actual is greater than the expected. type BeAboveMatcher struct { expected float64 } // BeAbove returns a BeAboveMatcher with the expected value. func BeAbove(expected float64) BeAboveMatcher { return BeAboveMatcher{ expected: expected, } } func (m BeAboveMatcher) Match(actual any) (any, error) { f, err := m.toFloat(actual) if err != nil { return nil, err } if f <= m.expected { return nil, fmt.Errorf("%v is not above %f", actual, m.expected) } return actual, nil } func (m BeAboveMatcher) toFloat(actual any) (float64, error) { switch x := actual.(type) { case int: return float64(x), nil case int32: return float64(x), nil case int64: return float64(x), nil case float32: return float64(x), nil case float64: return x, nil default: return 0, fmt.Errorf("Unsupported type %T", actual) } } onpar-0.3.3/matchers/be_above_test.go000066400000000000000000000014041457413705600176050ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestAbove(t *testing.T) { t.Parallel() m := matchers.BeAbove(101) _, err := m.Match(101.0) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(99.0) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(int(99)) if err == nil { t.Error("expected err to not be nil") } v, err := m.Match(103.0) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, 103.0) { t.Errorf("expected %v to equal %v", v, 103.0) } v, err = m.Match(int(103)) if err != nil { t.Error("expected err to be nil") } _, err = m.Match("invalid") if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/be_below.go000066400000000000000000000016311457413705600165640ustar00rootroot00000000000000package matchers import "fmt" // BeBelowMatcher accepts a float64. It succeeds if the actual is // less than the expected. type BeBelowMatcher struct { expected float64 } // BeBelow returns a BeBelowMatcher with the expected value. func BeBelow(expected float64) BeBelowMatcher { return BeBelowMatcher{ expected: expected, } } func (m BeBelowMatcher) Match(actual any) (any, error) { f, err := m.toFloat(actual) if err != nil { return nil, err } if f >= m.expected { return nil, fmt.Errorf("%v is not below %f", actual, m.expected) } return actual, nil } func (m BeBelowMatcher) toFloat(actual any) (float64, error) { switch x := actual.(type) { case int: return float64(x), nil case int32: return float64(x), nil case int64: return float64(x), nil case float32: return float64(x), nil case float64: return x, nil default: return 0, fmt.Errorf("Unsupported type %T", actual) } } onpar-0.3.3/matchers/be_below_test.go000066400000000000000000000014011457413705600176160ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestBelow(t *testing.T) { t.Parallel() m := matchers.BeBelow(101) v, err := m.Match(99.0) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, 99.0) { t.Errorf("expected %v to equal %v", v, 99.0) } v, err = m.Match(int(99)) if err != nil { t.Error("expected err to be nil") } _, err = m.Match(101.0) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(103.0) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(int(103)) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match("invalid") if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/be_closed.go000066400000000000000000000015111457413705600167220ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // BeClosedMatcher only accepts a readable channel. // It will error for anything else. // It will succeed if the channel is closed. type BeClosedMatcher struct{} // BeClosed returns a BeClosedMatcher func BeClosed() BeClosedMatcher { return BeClosedMatcher{} } func (m BeClosedMatcher) Match(actual any) (any, error) { t := reflect.TypeOf(actual) if t.Kind() != reflect.Chan || t.ChanDir() == reflect.SendDir { return nil, fmt.Errorf("%s is not a readable channel", t.String()) } v := reflect.ValueOf(actual) winnerIndex, _, open := reflect.Select([]reflect.SelectCase{ reflect.SelectCase{Dir: reflect.SelectRecv, Chan: v}, reflect.SelectCase{Dir: reflect.SelectDefault}, }) if winnerIndex == 0 && !open { return actual, nil } return nil, fmt.Errorf("channel open") } onpar-0.3.3/matchers/be_closed_test.go000066400000000000000000000010371457413705600177640ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestBeClosed(t *testing.T) { t.Parallel() m := matchers.BeClosed() c := make(chan int) close(c) v, err := m.Match(c) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, c) { t.Errorf("expected %v to equal %v", v, c) } c = make(chan int) _, err = m.Match(c) if err == nil { t.Errorf("expected err to not be nil") } _, err = m.Match(99) if err == nil { t.Errorf("expected err to not be nil") } } onpar-0.3.3/matchers/be_false.go000066400000000000000000000007111457413705600165440ustar00rootroot00000000000000package matchers import "fmt" // BeFalseMatcher will succeed if actual is false. type BeFalseMatcher struct{} // BeFalse will return a BeFalseMatcher func BeFalse() BeFalseMatcher { return BeFalseMatcher{} } func (m BeFalseMatcher) Match(actual any) (any, error) { f, ok := actual.(bool) if !ok { return nil, fmt.Errorf("'%v' (%[1]T) is not a bool", actual) } if f { return nil, fmt.Errorf("%t is not false", actual) } return actual, nil } onpar-0.3.3/matchers/be_false_test.go000066400000000000000000000011461457413705600176060ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestFalsiness(t *testing.T) { t.Parallel() m := matchers.BeFalse() _, err := m.Match(42) if err == nil { t.Error("expected err to not be nil") } v, err := m.Match(false) if err != nil { t.Error("expected err to be nil") } if v != false { t.Errorf("expected %v to be false", v) } _, err = m.Match(true) if err == nil { t.Error("expected err to not be nil") } v, err = m.Match(2 != 2) if err != nil { t.Error("expected err to be nil") } if v != false { t.Errorf("expected %v to be false", v) } } onpar-0.3.3/matchers/be_nil.go000066400000000000000000000011231457413705600162320ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // IsNilMatcher will succeed if actual is nil. type IsNilMatcher struct{} // IsNil will return a IsNilMatcher. func IsNil() IsNilMatcher { return IsNilMatcher{} } func (m IsNilMatcher) Match(actual any) (any, error) { if actual == nil { return nil, nil } var isNil bool switch reflect.TypeOf(actual).Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: isNil = reflect.ValueOf(actual).IsNil() } if isNil { return nil, nil } return actual, fmt.Errorf("%v is not nil", actual) } onpar-0.3.3/matchers/be_nil_test.go000066400000000000000000000011161457413705600172730ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestIsNil(t *testing.T) { t.Parallel() m := matchers.IsNil() _, err := m.Match(nil) if err != nil { t.Error("expected err to be nil") } v, err := m.Match(42) if err == nil { t.Error("expected err to not be nil") } if v != 42 { t.Errorf("expected %v to equal %d", v, 42) } var x map[string]string _, err = m.Match(x) if err != nil { t.Error("expected err to be nil") } x = map[string]string{} _, err = m.Match(x) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/be_true.go000066400000000000000000000006771457413705600164440ustar00rootroot00000000000000package matchers import "fmt" // BeTrueMatcher will succeed if actual is true. type BeTrueMatcher struct{} // BeTrue will return a BeTrueMatcher func BeTrue() BeTrueMatcher { return BeTrueMatcher{} } func (m BeTrueMatcher) Match(actual any) (any, error) { f, ok := actual.(bool) if !ok { return nil, fmt.Errorf("'%v' (%[1]T) is not a bool", actual) } if !f { return nil, fmt.Errorf("%t is not true", actual) } return actual, nil } onpar-0.3.3/matchers/be_true_test.go000066400000000000000000000011421457413705600174670ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestTruthiness(t *testing.T) { t.Parallel() m := matchers.BeTrue() _, err := m.Match(42) if err == nil { t.Error("expected err to not be nil") } v, err := m.Match(true) if err != nil { t.Error("expected err to be nil") } if v != true { t.Errorf("expected %v to be true", v) } _, err = m.Match(false) if err == nil { t.Error("expected err to not be nil") } v, err = m.Match(2 == 2) if err != nil { t.Error("expected err to be nil") } if v != true { t.Errorf("expected %v to be true", v) } } onpar-0.3.3/matchers/chain.go000066400000000000000000000011771457413705600160750ustar00rootroot00000000000000package matchers type DifferUser interface { UseDiffer(Differ) } type ChainMatcher struct { Children []Matcher differ Differ } func Chain(a, b Matcher, ms ...Matcher) *ChainMatcher { return &ChainMatcher{ Children: append(append([]Matcher{a}, b), ms...), } } func (m *ChainMatcher) UseDiffer(d Differ) { m.differ = d } func (m ChainMatcher) Match(actual any) (any, error) { var err error next := actual for _, child := range m.Children { if d, ok := child.(DifferUser); ok && m.differ != nil { d.UseDiffer(m.differ) } next, err = child.Match(next) if err != nil { return nil, err } } return next, nil } onpar-0.3.3/matchers/chain_test.go000066400000000000000000000037461457413705600171400ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/poy/onpar/matchers" ) func TestChainPassesResultOnSuccess(t *testing.T) { mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, nil) pers.Return(mockMatcherB.MatchOutput, 2, nil) pers.Return(mockMatcherC.MatchOutput, 3, nil) m := matchers.Chain(mockMatcherA, mockMatcherB, mockMatcherC) result, err := m.Match(101) if len(mockMatcherB.MatchInput.Actual) != 1 { t.Fatalf("expected Match() to be invoked") } if len(mockMatcherC.MatchInput.Actual) != 1 { t.Fatalf("expected Match() to be invoked") } bActual := <-mockMatcherB.MatchInput.Actual if !reflect.DeepEqual(bActual, 1) { t.Errorf("expected %v to equal %v", bActual, 1) } cActual := <-mockMatcherC.MatchInput.Actual if !reflect.DeepEqual(cActual, 2) { t.Errorf("expected %v to equal %v", cActual, 2) } if err != nil { t.Errorf("expected err to be nil") } if !reflect.DeepEqual(result, 3) { t.Errorf("expected %v to equal %v", result, 3) } } func TestChainStopsOnFailuire(t *testing.T) { mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, nil) pers.Return(mockMatcherB.MatchOutput, 2, fmt.Errorf("some-error")) pers.Return(mockMatcherC.MatchOutput, 3, nil) m := matchers.Chain(mockMatcherA, mockMatcherB, mockMatcherC) _, err := m.Match(101) if len(mockMatcherB.MatchInput.Actual) != 1 { t.Fatalf("expected Match() to be invoked") } if len(mockMatcherC.MatchInput.Actual) != 0 { t.Fatalf("expected Match() to not be invoked") } bActual := <-mockMatcherB.MatchInput.Actual if !reflect.DeepEqual(bActual, 1) { t.Errorf("expected %v to equal %v", bActual, 1) } if err == nil { t.Errorf("expected err to not be nil") } } onpar-0.3.3/matchers/contain.go000066400000000000000000000015371457413705600164460ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) type ContainMatcher struct { values []any } func Contain(values ...any) ContainMatcher { return ContainMatcher{ values: values, } } func (m ContainMatcher) Match(actual any) (any, error) { actualType := reflect.TypeOf(actual) if actualType.Kind() != reflect.Slice && actualType.Kind() != reflect.Array { return nil, fmt.Errorf("%s is not a Slice or Array", actualType.Kind()) } actualValue := reflect.ValueOf(actual) for _, elem := range m.values { if !m.containsElem(actualValue, elem) { return nil, fmt.Errorf("%v does not contain %v", actual, elem) } } return actual, nil } func (m ContainMatcher) containsElem(actual reflect.Value, elem any) bool { for i := 0; i < actual.Len(); i++ { if reflect.DeepEqual(actual.Index(i).Interface(), elem) { return true } } return false } onpar-0.3.3/matchers/contain_sub_string.go000066400000000000000000000013241457413705600206770ustar00rootroot00000000000000package matchers import ( "fmt" "strings" ) // ContainSubstringMatcher accepts a string and succeeds // if the actual string contains the expected string. type ContainSubstringMatcher struct { substr string } // ContainSubstring returns a ContainSubstringMatcher with the // expected substring. func ContainSubstring(substr string) ContainSubstringMatcher { return ContainSubstringMatcher{ substr: substr, } } func (m ContainSubstringMatcher) Match(actual any) (any, error) { s, ok := actual.(string) if !ok { return nil, fmt.Errorf("'%v' (%T) is not a string", actual, actual) } if !strings.Contains(s, m.substr) { return nil, fmt.Errorf("%s does not contain %s", s, m.substr) } return actual, nil } onpar-0.3.3/matchers/contain_sub_string_test.go000066400000000000000000000010331457413705600217330ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestContainSubstring(t *testing.T) { t.Parallel() m := matchers.ContainSubstring("foo") _, err := m.Match("bar") if err == nil { t.Error("expected err to not be nil") } v, err := m.Match("foobar") if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, "foobar") { t.Errorf("expected %v to equal %v", v, "foobar") } _, err = m.Match(101) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/contain_test.go000066400000000000000000000011061457413705600174750ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestContain(t *testing.T) { t.Parallel() m := matchers.Contain("a", 1) values := []any{"a", "b", "c", 1, 2, 3} v, err := m.Match(values) if err != nil { t.Fatalf("expected err (%s) to be nil", err) } if !reflect.DeepEqual(v, values) { t.Fatalf("expected %v to equal %v", v, values) } _, err = m.Match([]any{"d", "e"}) if err == nil { t.Fatal("expected err to not be nil") } _, err = m.Match("invalid") if err == nil { t.Fatal("expected err to not be nil") } } onpar-0.3.3/matchers/differ.go000066400000000000000000000002631457413705600162450ustar00rootroot00000000000000package matchers // Differ is a type that can show a detailed difference between an // actual and an expected value. type Differ interface { Diff(actual, expected any) string } onpar-0.3.3/matchers/end_with.go000066400000000000000000000012121457413705600166020ustar00rootroot00000000000000package matchers import ( "fmt" "strings" ) // EndWithMatcher accepts a string and succeeds // if the actual string ends with the expected string. type EndWithMatcher struct { suffix string } // EndWith returns an EndWithMatcher with the expected suffix. func EndWith(suffix string) EndWithMatcher { return EndWithMatcher{ suffix: suffix, } } func (m EndWithMatcher) Match(actual any) (any, error) { s, ok := actual.(string) if !ok { return nil, fmt.Errorf("'%v' (%T) is not a string", actual, actual) } if !strings.HasSuffix(s, m.suffix) { return nil, fmt.Errorf("%s does not end with %s", s, m.suffix) } return actual, nil } onpar-0.3.3/matchers/end_with_test.go000066400000000000000000000010111457413705600176360ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestEndWith(t *testing.T) { t.Parallel() m := matchers.EndWith("foo") _, err := m.Match("bar") if err == nil { t.Error("expected err to not be nil") } v, err := m.Match("barfoo") if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, "barfoo") { t.Errorf("expected %v to equal %v", v, "barfoo") } _, err = m.Match(101) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/equal.go000066400000000000000000000013571457413705600161220ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // EqualMatcher performs a DeepEqual between the actual and expected. type EqualMatcher struct { expected any differ Differ } // Equal returns an EqualMatcher with the expected value func Equal(expected any) *EqualMatcher { return &EqualMatcher{ expected: expected, } } func (m *EqualMatcher) UseDiffer(d Differ) { m.differ = d } func (m EqualMatcher) Match(actual any) (any, error) { if !reflect.DeepEqual(actual, m.expected) { if m.differ == nil { return nil, fmt.Errorf("expected %+v (%[1]T) to equal %+v (%[2]T)", actual, m.expected) } return nil, fmt.Errorf("expected %v to equal %v\ndiff: %s", actual, m.expected, m.differ.Diff(actual, m.expected)) } return actual, nil } onpar-0.3.3/matchers/equal_test.go000066400000000000000000000020511457413705600171510ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/poy/onpar/expect" "github.com/poy/onpar/matchers" ) func TestEqual(t *testing.T) { t.Parallel() m := matchers.Equal(101) v, err := m.Match(101) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, 101) { t.Errorf("expected %v to equal %v", v, 101) } _, err = m.Match(103) if err == nil { t.Fatalf("expected %v to not be nil", err) } } func TestEqualDiff(t *testing.T) { t.Parallel() m := matchers.Equal(101) mockDiffer := newMockDiffer(t, time.Second) pers.Return(mockDiffer.DiffOutput, "this is a valid diff") m.UseDiffer(mockDiffer) _, err := m.Match(103) if err == nil { t.Fatalf("expected %v to not be nil", err) } expect.Expect(t, mockDiffer).To(pers.HaveMethodExecuted("Diff", pers.WithArgs(103, 101))) format := fmt.Sprintf("expected 103 to equal 101\ndiff: this is a valid diff") if err.Error() != format { t.Fatalf("expected '%v' to match '%v'", err.Error(), format) } } onpar-0.3.3/matchers/fetch.go000066400000000000000000000013641457413705600161020ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) type FetchMatcher struct { OutputTo any } func Fetch(outputTo any) FetchMatcher { return FetchMatcher{ OutputTo: outputTo, } } func (m FetchMatcher) Match(actual any) (any, error) { outType := reflect.TypeOf(m.OutputTo) outValue := reflect.ValueOf(m.OutputTo) actualValue := reflect.ValueOf(actual) if outType.Kind() != reflect.Ptr { return nil, fmt.Errorf("%s is not a pointer type", outType.String()) } if !reflect.TypeOf(actualValue.Interface()).AssignableTo(outType.Elem()) { return nil, fmt.Errorf("can not assigned %s to %s", reflect.TypeOf(actualValue.Interface()).String(), reflect.TypeOf(m.OutputTo).String(), ) } outValue.Elem().Set(actualValue) return actual, nil } onpar-0.3.3/matchers/fetch_test.go000066400000000000000000000011621457413705600171350ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestFetchMatcher(t *testing.T) { t.Parallel() var i int m := matchers.Fetch(&i) v, err := m.Match(99) if err != nil { t.Fatal("expected err to be nil") } if !reflect.DeepEqual(v, 99) { t.Fatalf("expected %v to equal %v", v, 99) } if !reflect.DeepEqual(i, 99) { t.Fatalf("expected %v to equal %v", i, 99) } _, err = m.Match("invalid") if err == nil { t.Fatal("expected err to not equal nil") } m = matchers.Fetch(i) _, err = m.Match(99) if err == nil { t.Fatal("expected err to not equal nil") } } onpar-0.3.3/matchers/have_cap.go000066400000000000000000000014711457413705600165560ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // This matcher works on Slices, Arrays, Maps and Channels and will succeed if the // type has the specified capacity. type HaveCapMatcher struct { expected int } // HaveCap returns a HaveCapMatcher with the specified capacity func HaveCap(expected int) HaveCapMatcher { return HaveCapMatcher{ expected: expected, } } func (m HaveCapMatcher) Match(actual any) (any, error) { var c int switch reflect.TypeOf(actual).Kind() { case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: c = reflect.ValueOf(actual).Cap() default: return nil, fmt.Errorf("'%v' (%T) is not a Slice, Array, Map or Channel", actual, actual) } if c != m.expected { return nil, fmt.Errorf("%v (cap=%d) does not have a capacity %d", actual, c, m.expected) } return actual, nil } onpar-0.3.3/matchers/have_cap_test.go000066400000000000000000000014471457413705600176200ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestCap(t *testing.T) { m := matchers.HaveCap(5) _, err := m.Match(make([]int, 0, 3)) if err == nil { t.Error("expected err to not be nil") } x := make([]int, 0, 5) v, err := m.Match(x) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, x) { t.Errorf("expected %v to equal %v", v, x) } _, err = m.Match(make(chan int, 3)) if err == nil { t.Error("expected err to not be nil") } c := make(chan int, 5) _, err = m.Match(c) if err != nil { t.Error("expected err to be nil") } _, err = m.Match(123) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match("some string") if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/have_key.go000066400000000000000000000015331457413705600166020ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // HaveKeyMatcher accepts map types and will succeed if the map contains the // specified key. type HaveKeyMatcher struct { key any } // HaveKey returns a HaveKeyMatcher with the specified key. func HaveKey(key any) HaveKeyMatcher { return HaveKeyMatcher{ key: key, } } func (m HaveKeyMatcher) Match(actual any) (any, error) { t := reflect.TypeOf(actual) if t.Kind() != reflect.Map { return nil, fmt.Errorf("'%v' (%T) is not a Map", actual, actual) } if t.Key() != reflect.TypeOf(m.key) { return nil, fmt.Errorf("'%v' (%T) has a Key type of %v not %T", actual, actual, t.Key(), m.key) } v := reflect.ValueOf(actual) value := v.MapIndex(reflect.ValueOf(m.key)) if !value.IsValid() { return nil, fmt.Errorf("unable to find key %v in %v", m.key, actual) } return value.Interface(), nil } onpar-0.3.3/matchers/have_key_test.go000066400000000000000000000011361457413705600176400ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestHaveKey(t *testing.T) { m := matchers.HaveKey(99) _, err := m.Match(map[int]string{1: "a"}) if err == nil { t.Error("expected err to not be nil") } v, err := m.Match(map[int]string{99: "a"}) if err != nil { t.Fatal("expected err to be nil") } if v != "a" { t.Errorf("expected %v to equal %v", v, "a") } _, err = m.Match(map[string]string{"foo": "a"}) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(101) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/have_len.go000066400000000000000000000015311457413705600165660ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // HaveLenMatcher accepts Strings, Slices, Arrays, Maps and Channels. It will // succeed if the type has the specified length. type HaveLenMatcher struct { expected int } // HaveLen returns a HaveLenMatcher with the specified length. func HaveLen(expected int) HaveLenMatcher { return HaveLenMatcher{ expected: expected, } } func (m HaveLenMatcher) Match(actual any) (any, error) { var l int switch reflect.TypeOf(actual).Kind() { case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan: l = reflect.ValueOf(actual).Len() default: return nil, fmt.Errorf("'%v' (%T) is not a Slice, Array, Map, String or Channel", actual, actual) } if l != m.expected { return nil, fmt.Errorf("%v (len=%d) does not have a length of %d", actual, l, m.expected) } return actual, nil } onpar-0.3.3/matchers/have_len_test.go000066400000000000000000000022301457413705600176220ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestLen(t *testing.T) { m := matchers.HaveLen(5) _, err := m.Match([]int{1, 2, 3}) if err == nil { t.Error("expected err to not be nil") } x := []int{1, 2, 3, 4, 5} v, err := m.Match(x) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, x) { t.Errorf("expected %v to equal %v", v, x) } _, err = m.Match(make(chan int, 3)) if err == nil { t.Error("expected err to not be nil") } c := make(chan int, 10) for i := 0; i < 5; i++ { c <- i } _, err = m.Match(c) if err != nil { t.Error("expected err to be nil") } _, err = m.Match(map[int]bool{1: true, 2: true, 3: true}) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(map[int]bool{1: true, 2: true, 3: true, 4: true, 5: true}) if err != nil { t.Error("expected err to be nil") } _, err = m.Match("123") if err == nil { t.Error("expected err to not be nil") } _, err = m.Match("12345") if err != nil { t.Error("expected err to be nil") } _, err = m.Match(123) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/have_occurred.go000066400000000000000000000010111457413705600176070ustar00rootroot00000000000000package matchers import "fmt" // HaveOccurredMatcher will succeed if the actual value is a non-nil error. type HaveOccurredMatcher struct { } // HaveOccurred returns a HaveOccurredMatcher func HaveOccurred() HaveOccurredMatcher { return HaveOccurredMatcher{} } func (m HaveOccurredMatcher) Match(actual any) (any, error) { e, ok := actual.(error) if !ok { return nil, fmt.Errorf("'%v' (%T) is not an error", actual, actual) } if e == nil { return nil, fmt.Errorf("err to not be nil") } return nil, nil } onpar-0.3.3/matchers/have_occurred_test.go000066400000000000000000000006641457413705600206630ustar00rootroot00000000000000package matchers_test import ( "fmt" "testing" "github.com/poy/onpar/matchers" ) func TestHaveOccurred(t *testing.T) { m := matchers.HaveOccurred() _, err := m.Match(fmt.Errorf("some-err")) if err != nil { t.Fatal("expected err to be nil") } var e error _, err = m.Match(e) if err == nil { t.Fatal("expected err to not be nil") } _, err = m.Match(nil) if err == nil { t.Fatal("expected err to not be nil") } } onpar-0.3.3/matchers/helheim_test.go000066400000000000000000000034571457413705600174700ustar00rootroot00000000000000// Code generated by git.sr.ht/~nelsam/hel. DO NOT EDIT. // // This file contains mocks generated by hel. Do not edit this code by // hand unless you *really* know what you're doing. Expect any changes // made manually to be overwritten the next time hel regenerates this // file. package matchers_test import ( "time" "git.sr.ht/~nelsam/hel/vegr" ) type mockDiffer struct { t vegr.T timeout time.Duration DiffCalled chan bool DiffInput struct { Actual, Expected chan any } DiffOutput struct { Ret0 chan string } } func newMockDiffer(t vegr.T, timeout time.Duration) *mockDiffer { m := &mockDiffer{t: t, timeout: timeout} m.DiffCalled = make(chan bool, 100) m.DiffInput.Actual = make(chan any, 100) m.DiffInput.Expected = make(chan any, 100) m.DiffOutput.Ret0 = make(chan string, 100) return m } func (m *mockDiffer) Diff(actual, expected any) (ret0 string) { m.t.Helper() m.DiffCalled <- true m.DiffInput.Actual <- actual m.DiffInput.Expected <- expected vegr.PopulateReturns(m.t, "Diff", m.timeout, m.DiffOutput, &ret0) return ret0 } type mockMatcher struct { t vegr.T timeout time.Duration MatchCalled chan bool MatchInput struct { Actual chan any } MatchOutput struct { ResultValue chan any Err chan error } } func newMockMatcher(t vegr.T, timeout time.Duration) *mockMatcher { m := &mockMatcher{t: t, timeout: timeout} m.MatchCalled = make(chan bool, 100) m.MatchInput.Actual = make(chan any, 100) m.MatchOutput.ResultValue = make(chan any, 100) m.MatchOutput.Err = make(chan error, 100) return m } func (m *mockMatcher) Match(actual any) (resultValue any, err error) { m.t.Helper() m.MatchCalled <- true m.MatchInput.Actual <- actual vegr.PopulateReturns(m.t, "Match", m.timeout, m.MatchOutput, &resultValue, &err) return resultValue, err } onpar-0.3.3/matchers/is_nil.go000066400000000000000000000011231457413705600162570ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" ) // BeNilMatcher will succeed if actual is nil. type BeNilMatcher struct{} // BeNil will return a BeNilMatcher. func BeNil() BeNilMatcher { return BeNilMatcher{} } func (m BeNilMatcher) Match(actual any) (any, error) { if actual == nil { return nil, nil } var isNil bool switch reflect.TypeOf(actual).Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: isNil = reflect.ValueOf(actual).IsNil() } if isNil { return nil, nil } return actual, fmt.Errorf("%v is not nil", actual) } onpar-0.3.3/matchers/is_nil_test.go000066400000000000000000000011161457413705600173200ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestBeNil(t *testing.T) { t.Parallel() m := matchers.BeNil() _, err := m.Match(nil) if err != nil { t.Error("expected err to be nil") } v, err := m.Match(42) if err == nil { t.Error("expected err to not be nil") } if v != 42 { t.Errorf("expected %v to equal %d", v, 42) } var x map[string]string _, err = m.Match(x) if err != nil { t.Error("expected err to be nil") } x = map[string]string{} _, err = m.Match(x) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/match_json.go000066400000000000000000000026461457413705600171420ustar00rootroot00000000000000package matchers import ( "encoding/json" "fmt" "reflect" ) // MatchJSONMatcher converts both expected and actual to a map[string]any // and does a reflect.DeepEqual between them type MatchJSONMatcher struct { expected any } // MatchJSON returns an MatchJSONMatcher with the expected value func MatchJSON(expected any) MatchJSONMatcher { return MatchJSONMatcher{ expected: expected, } } func (m MatchJSONMatcher) Match(actual any) (any, error) { a, sa, err := m.unmarshal(actual) if err != nil { return nil, fmt.Errorf("Error with %s: %s", sa, err) } e, se, err := m.unmarshal(m.expected) if err != nil { return nil, fmt.Errorf("Error with %s: %s", se, err) } if !reflect.DeepEqual(a, e) { return nil, fmt.Errorf("expected %s to equal %s", sa, se) } return actual, nil } func (m MatchJSONMatcher) unmarshal(x any) (any, string, error) { var result any var s string switch x := x.(type) { case []byte: if err := json.Unmarshal(x, &result); err != nil { return nil, string(x), err } s = string(x) case string: if err := json.Unmarshal([]byte(x), &result); err != nil { return nil, x, err } s = x case *string: if x == nil { return nil, "", fmt.Errorf("*string cannot be nil") } s = *x if err := json.Unmarshal([]byte(s), &result); err != nil { return nil, s, err } default: return nil, "", fmt.Errorf("must be a []byte, *string, or string") } return result, s, nil } onpar-0.3.3/matchers/match_json_test.go000066400000000000000000000050751457413705600202000ustar00rootroot00000000000000package matchers_test import ( "reflect" "strings" "testing" "github.com/poy/onpar/matchers" ) func TestMatchJSON(t *testing.T) { t.Parallel() t.Run("object", func(t *testing.T) { obj := `{"a": 99}` m := matchers.MatchJSON([]byte(obj)) v, err := m.Match(obj) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, obj) { t.Errorf("expected %v to equal %v", v, obj) } if _, err := m.Match(`{"different": 99}`); err == nil { t.Fatalf("expected %v to not be nil", err) } }) t.Run("list", func(t *testing.T) { list := `[1, 2, 3]` m := matchers.MatchJSON(list) v, err := m.Match(list) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, list) { t.Errorf("expected %v to equal %v", v, list) } if _, err := m.Match(`[3, 2, 1]`); err == nil { t.Fatalf("expected %v to not be nil", err) } }) t.Run("string", func(t *testing.T) { str := `"foo"` m := matchers.MatchJSON(str) v, err := m.Match(str) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, str) { t.Errorf("expected %v to equal %v", v, str) } if _, err := m.Match(`"bar"`); err == nil { t.Fatalf("expected %v to not be nil", err) } }) t.Run("numeric", func(t *testing.T) { num := `42` m := matchers.MatchJSON(num) v, err := m.Match(num) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, num) { t.Errorf("expected %v to equal %v", v, num) } if _, err := m.Match(`3.7`); err == nil { t.Fatalf("expected %v to not be nil", err) } }) t.Run("boolean", func(t *testing.T) { bool := `true` m := matchers.MatchJSON(bool) v, err := m.Match(bool) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, bool) { t.Errorf("expected %v to equal %v", v, bool) } if _, err := m.Match(`false`); err == nil { t.Fatalf("expected %v to not be nil", err) } }) t.Run("match string pointer", func(t *testing.T) { obj := `{"a": 99}` m := matchers.MatchJSON(&obj) v, err := m.Match(obj) if err != nil { t.Errorf("expected %v to be nil", err) } if !reflect.DeepEqual(v, obj) { t.Errorf("expected %v to equal %v", v, obj) } if _, err := m.Match(`{"different": 99}`); err == nil { t.Fatalf("expected %v to not be nil", err) } var nilStr *string _, err = m.Match(nilStr) const expected = "*string cannot be nil" if !strings.Contains(err.Error(), expected) { t.Fatalf("expected error to contain `%s` but got `%v`", expected, err) } }) } onpar-0.3.3/matchers/match_regexp.go000066400000000000000000000010771457413705600174600ustar00rootroot00000000000000package matchers import ( "fmt" "regexp" ) type MatchRegexpMatcher struct { pattern string } func MatchRegexp(pattern string) MatchRegexpMatcher { return MatchRegexpMatcher{ pattern: pattern, } } func (m MatchRegexpMatcher) Match(actual any) (any, error) { r, err := regexp.Compile(m.pattern) if err != nil { return nil, err } s, ok := actual.(string) if !ok { return nil, fmt.Errorf("'%v' (%T) is not a string", actual, actual) } if !r.MatchString(s) { return nil, fmt.Errorf("%s does not match pattern %s", s, m.pattern) } return actual, nil } onpar-0.3.3/matchers/match_regexp_test.go000066400000000000000000000010201457413705600205030ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestRegexp(t *testing.T) { t.Parallel() m := matchers.MatchRegexp("^foo") _, err := m.Match("barfoo") if err == nil { t.Error("expected err to not be nil") } v, err := m.Match("foobar") if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, "foobar") { t.Errorf("expected %v to equal %v", v, "foobar") } _, err = m.Match(101) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/not.go000066400000000000000000000012071457413705600156050ustar00rootroot00000000000000package matchers import "fmt" // Matcher is a type that matches expected against actuals. type Matcher interface { Match(actual any) (resultValue any, err error) } // NotMatcher accepts a matcher and will succeed if the specified matcher fails. type NotMatcher struct { child Matcher } // Not returns a NotMatcher with the specified child matcher. func Not(child Matcher) NotMatcher { return NotMatcher{ child: child, } } func (m NotMatcher) Match(actual any) (any, error) { v, err := m.child.Match(actual) if err == nil { return nil, fmt.Errorf("%+v (%[1]T) was expected to fail matcher %#v", actual, m.child) } return v, nil } onpar-0.3.3/matchers/not_test.go000066400000000000000000000016321457413705600166460ustar00rootroot00000000000000//go:generate hel package matchers_test import ( "fmt" "reflect" "testing" "time" "github.com/poy/onpar/matchers" ) func TestNot(t *testing.T) { mockMatcher := newMockMatcher(t, time.Second) m := matchers.Not(mockMatcher) mockMatcher.MatchOutput.ResultValue <- 103 mockMatcher.MatchOutput.Err <- nil _, err := m.Match(101) if err == nil { t.Error("expected err to not be nil") } if len(mockMatcher.MatchInput.Actual) != 1 { t.Fatal("expected child macther Match() to be invoked once") } actual := <-mockMatcher.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Fatalf("expected %v does not equal %v", actual, 101) } mockMatcher.MatchOutput.ResultValue <- 103 mockMatcher.MatchOutput.Err <- fmt.Errorf("some-error") v, err := m.Match(101) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, 103) { t.Errorf("expected %v to equal %v", v, 103) } } onpar-0.3.3/matchers/or.go000066400000000000000000000006071457413705600154300ustar00rootroot00000000000000package matchers type OrMatcher struct { Children []Matcher } func Or(a, b Matcher, ms ...Matcher) OrMatcher { return OrMatcher{ Children: append(append([]Matcher{a}, b), ms...), } } func (m OrMatcher) Match(actual any) (any, error) { var err error for _, child := range m.Children { _, err = child.Match(actual) if err == nil { return actual, nil } } return nil, err } onpar-0.3.3/matchers/or_test.go000066400000000000000000000037071457413705600164730ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/poy/onpar/matchers" ) func TestOrFailureUsesEachMatcher(t *testing.T) { t.Parallel() mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, fmt.Errorf("some-error")) pers.Return(mockMatcherB.MatchOutput, 2, fmt.Errorf("some-error")) pers.Return(mockMatcherC.MatchOutput, 3, fmt.Errorf("some-error")) m := matchers.Or(mockMatcherA, mockMatcherB, mockMatcherC) _, err := m.Match(101) if err == nil { t.Error("expected err to not be nil") } actual := <-mockMatcherA.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } actual = <-mockMatcherB.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } actual = <-mockMatcherC.MatchInput.Actual if !reflect.DeepEqual(actual, 101) { t.Errorf("expecte %v to equal %v", actual, 101) } } func TestOrStopsOnSuccess(t *testing.T) { t.Parallel() mockMatcherA := newMockMatcher(t, time.Second) mockMatcherB := newMockMatcher(t, time.Second) mockMatcherC := newMockMatcher(t, time.Second) pers.Return(mockMatcherA.MatchOutput, 1, fmt.Errorf("some-error")) pers.Return(mockMatcherB.MatchOutput, 2, nil) pers.Return(mockMatcherC.MatchOutput, 3, nil) m := matchers.Or(mockMatcherA, mockMatcherB, mockMatcherC) v, err := m.Match(101) if err != nil { t.Error("expected err to be nil") } if len(mockMatcherA.MatchCalled) != 1 { t.Errorf("expected Match() to be invoked 1 time") } if len(mockMatcherB.MatchCalled) != 1 { t.Errorf("expected Match() to be invoked 1 time") } if len(mockMatcherC.MatchCalled) != 0 { t.Errorf("expected Match() to be invoked 0 times") } if !reflect.DeepEqual(v, 101) { t.Errorf("expecte %v to equal %v", v, 101) } } onpar-0.3.3/matchers/panic.go000066400000000000000000000007741457413705600161070ustar00rootroot00000000000000package matchers import "errors" // PanicMatcher accepts a function. It succeeds if the function panics. type PanicMatcher struct { } // Panic returns a Panic matcher. func Panic() PanicMatcher { return PanicMatcher{} } func (m PanicMatcher) Match(actual any) (result any, err error) { f, ok := actual.(func()) if !ok { return nil, errors.New("actual must be a func()") } defer func() { r := recover() if r == nil { err = errors.New("expected to panic") } }() f() return nil, nil } onpar-0.3.3/matchers/panic_test.go000066400000000000000000000010101457413705600171260ustar00rootroot00000000000000package matchers_test import ( "testing" "github.com/poy/onpar/matchers" ) func TestPanic(t *testing.T) { t.Parallel() m := matchers.Panic() _, err := m.Match(func() { panic("error") }) if err != nil { t.Error("expected err to be nil") } _, err = m.Match(101.0) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(func() {}) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match("invalid") if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/receive.go000066400000000000000000000027561457413705600164410ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" "time" ) // ReceiveOpt is an option that can be passed to the // ReceiveMatcher constructor. type ReceiveOpt func(ReceiveMatcher) ReceiveMatcher // ReceiveWait is an option that makes the ReceiveMatcher // wait for values for the provided duration before // deciding that the channel failed to receive. func ReceiveWait(t time.Duration) ReceiveOpt { return func(m ReceiveMatcher) ReceiveMatcher { m.timeout = t return m } } // ReceiveMatcher only accepts a readable channel. It will error for anything else. // It will attempt to receive from the channel but will not block. // It fails if the channel is closed. type ReceiveMatcher struct { timeout time.Duration } // Receive will return a ReceiveMatcher func Receive(opts ...ReceiveOpt) ReceiveMatcher { var m ReceiveMatcher for _, opt := range opts { m = opt(m) } return m } func (m ReceiveMatcher) Match(actual any) (any, error) { t := reflect.TypeOf(actual) if t.Kind() != reflect.Chan || t.ChanDir() == reflect.SendDir { return nil, fmt.Errorf("%s is not a readable channel", t.String()) } timeout := reflect.SelectCase{ Dir: reflect.SelectDefault, } if m.timeout != 0 { timeout.Dir = reflect.SelectRecv timeout.Chan = reflect.ValueOf(time.After(m.timeout)) } i, v, ok := reflect.Select([]reflect.SelectCase{ {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(actual)}, timeout, }) if i == 1 || !ok { return nil, fmt.Errorf("did not receive") } return v.Interface(), nil } onpar-0.3.3/matchers/receive_test.go000066400000000000000000000037061457413705600174740ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "time" "github.com/poy/onpar/matchers" ) func TestReceiveSucceedsForABufferedChannel(t *testing.T) { t.Parallel() c := make(chan bool, 1) m := matchers.Receive() _, err := m.Match(c) if err == nil { t.Error("expected err to not be nil") } c <- true result, err := m.Match(c) if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(result, true) { t.Errorf("expected %v to equal %v", result, true) } } func TestReceiveWaitSucceedsForAnUnbufferedChannelInALoop(t *testing.T) { t.Parallel() c := make(chan bool) done := make(chan struct{}) defer close(done) go func() { for { select { case c <- true: case <-done: return default: // Receive should still be able to get a value from c, // even in this buggy loop. Onpar is for detecting // bugs, after all. } } }() m := matchers.Receive(matchers.ReceiveWait(500 * time.Millisecond)) _, err := m.Match(c) if err != nil { t.Errorf("expected err to be nil; got %s", err) } } func TestReceiveWaitSucceedsEventually(t *testing.T) { t.Parallel() c := make(chan bool) go func() { time.Sleep(200 * time.Millisecond) c <- true }() m := matchers.Receive(matchers.ReceiveWait(500 * time.Millisecond)) _, err := m.Match(c) if err != nil { t.Errorf("expected err to be nil; got %s", err) } } func TestReceiveFailsNotReadableChan(t *testing.T) { t.Parallel() c := make(chan int, 10) m := matchers.Receive() _, err := m.Match(101) if err == nil { t.Error("expected err to not be nil") } _, err = m.Match(chan<- int(c)) if err == nil { t.Error("expected err to not be nil") } } func TestReceiveFailsForClosedChannel(t *testing.T) { t.Parallel() c := make(chan bool, 1) m := matchers.Receive() c <- true _, err := m.Match(c) if err != nil { t.Error("expected err to be nil") } close(c) _, err = m.Match(c) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/start_with.go000066400000000000000000000012351457413705600171760ustar00rootroot00000000000000package matchers import ( "fmt" "strings" ) // StartWithMatcher accepts a string and succeeds // if the actual string starts with the expected string. type StartWithMatcher struct { prefix string } // StartWith returns a StartWithMatcher with the expected prefix. func StartWith(prefix string) StartWithMatcher { return StartWithMatcher{ prefix: prefix, } } func (m StartWithMatcher) Match(actual any) (any, error) { s, ok := actual.(string) if !ok { return nil, fmt.Errorf("'%v' (%T) is not a string", actual, actual) } if !strings.HasPrefix(s, m.prefix) { return nil, fmt.Errorf("%s does not start with %s", s, m.prefix) } return actual, nil } onpar-0.3.3/matchers/start_with_test.go000066400000000000000000000010151457413705600202310ustar00rootroot00000000000000package matchers_test import ( "reflect" "testing" "github.com/poy/onpar/matchers" ) func TestStartWith(t *testing.T) { t.Parallel() m := matchers.StartWith("foo") _, err := m.Match("bar") if err == nil { t.Error("expected err to not be nil") } v, err := m.Match("foobar") if err != nil { t.Error("expected err to be nil") } if !reflect.DeepEqual(v, "foobar") { t.Errorf("expected %v to equal %v", v, "foobar") } _, err = m.Match(101) if err == nil { t.Error("expected err to not be nil") } } onpar-0.3.3/matchers/via_polling.go000066400000000000000000000047101457413705600173120ustar00rootroot00000000000000package matchers import ( "fmt" "reflect" "time" ) // ViaPollingMatcher matches by polling the child matcher until // it returns a success. It will return success the first time // the child matcher returns a success. If the child matcher // never returns a nil, then it will return the last error. // // Duration is the worst case scenario for the matcher // if the child matcher continues to return an error // // Interval is the period between polling. type ViaPollingMatcher struct { Matcher Matcher Duration, Interval time.Duration } // ViaPolling returns the default ViaPollingMatcher. Length of 1s // and Rate of 10ms func ViaPolling(m Matcher) ViaPollingMatcher { return ViaPollingMatcher{ Matcher: m, } } // Match takes a value that can change over time. Therefore, the only // two valid options are a function with no arguments and a single return // type, or a readable channel. Anything else will return an error. // // If actual is a channel, then the child matcher will have to handle // reading from the channel. // // If the actual is a function, then the matcher will invoke the value // and pass the returned value to the child matcher. func (m ViaPollingMatcher) Match(actual any) (any, error) { if m.Duration == 0 { m.Duration = time.Second } if m.Interval == 0 { m.Interval = 10 * time.Millisecond } f, err := fetchFunc(actual) if err != nil { return nil, err } var value any for i := 0; i < int(m.Duration/m.Interval); i++ { value, err = m.Matcher.Match(f()) if err == nil { return value, nil } time.Sleep(m.Interval) } return nil, err } func fetchFunc(actual any) (func() any, error) { t := reflect.TypeOf(actual) switch t.Kind() { case reflect.Func: return fetchFuncFromFunc(actual) case reflect.Chan: return fetchFuncFromChan(actual) default: return nil, fmt.Errorf("invalid type: %v", t) } } func fetchFuncFromChan(actual any) (func() any, error) { t := reflect.TypeOf(actual) if t.ChanDir() == reflect.SendDir { return nil, fmt.Errorf("channel must be able to receive") } return func() any { return actual }, nil } func fetchFuncFromFunc(actual any) (func() any, error) { t := reflect.TypeOf(actual) if t.NumIn() != 0 { return nil, fmt.Errorf("func must not take any arguments") } if t.NumOut() != 1 { return nil, fmt.Errorf("func must have one return type") } return func() any { v := reflect.ValueOf(actual) retValues := v.Call(nil) return retValues[0].Interface() }, nil } onpar-0.3.3/matchers/via_polling_test.go000066400000000000000000000102621457413705600203500ustar00rootroot00000000000000package matchers_test import ( "fmt" "reflect" "testing" "time" "github.com/poy/onpar/matchers" ) func TestViaPollingFailsPolls100Times(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) callCount := make(chan bool, 200) f := func() int { callCount <- true matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- fmt.Errorf("still wrong") return 99 } m.Match(f) if len(callCount) != 100 { t.Errorf("expected callCount (len=%d) to have a len of %d", len(callCount), 100) } } func TestViaPollingPollsEvery10ms(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) var ts []int64 f := func() int { ts = append(ts, time.Now().UnixNano()) matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- fmt.Errorf("still wrong") return 99 } m.Match(f) for i := 0; i < len(ts)-1; i++ { if ts[i+1]-ts[i] < int64(10*time.Millisecond) || ts[i+1]-ts[i] > int64(30*time.Millisecond) { t.Fatalf("expected %d to be within 10ms and 30ms", ts[i+1]-ts[i]) } } } func TestViaPollingStopsAfterSuccess(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) i := 0 callCount := make(chan bool, 200) f := func() int { i++ callCount <- true matcher.MatchOutput.ResultValue <- i matcher.MatchOutput.Err <- nil return i } v, err := m.Match(f) if err != nil { t.Fatal("expected err to be nil") } if !reflect.DeepEqual(v, 1) { t.Errorf("expected %v to equal %v", v, 1) } if len(callCount) != 1 { t.Errorf("expected callCount (len=%d) to have a len of %d", len(callCount), 1) } } func TestViaPollingUsesGivenProperties(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPollingMatcher{ Matcher: matcher, Duration: time.Millisecond, Interval: 100 * time.Microsecond, } callCount := make(chan bool, 200) f := func() int { callCount <- true matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- fmt.Errorf("still wrong") return 99 } m.Match(f) if len(callCount) != 10 { t.Errorf("expected callCount (len=%d) to have a len of %d", len(callCount), 10) } } func TestViaPollingPassesAlongChan(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) matcher.MatchOutput.ResultValue <- nil matcher.MatchOutput.Err <- nil c := make(chan int) m.Match(c) if len(matcher.MatchInput.Actual) != 1 { t.Errorf("expected Actual (len=%d) to have a len of %d", len(matcher.MatchInput.Actual), 1) } actual := <-matcher.MatchInput.Actual if !reflect.DeepEqual(actual, c) { t.Errorf("expected %v to equal %v", actual, c) } } func TestViaPollingFailsForNonChanOrFunc(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) _, err := m.Match(101) if err == nil { t.Error("expected err to not be nil") } } func TestViaPollingFailsForFuncWithArgs(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) _, err := m.Match(func(int) int { return 101 }) if err == nil { t.Error("expected err to not be nil") } } func TestViaPollingFailsForFuncWithWrongReturns(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) _, err := m.Match(func() (int, int) { return 101, 103 }) if err == nil { t.Error("expected err to not be nil") } } func TestViaPollingFailsForSendOnlyChan(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) _, err := m.Match(make(chan<- int)) if err == nil { t.Error("expected err to not be nil") } } func TestViaPollingUsesChildMatcherErr(t *testing.T) { t.Parallel() matcher := newMockMatcher(t, time.Second) m := matchers.ViaPolling(matcher) f := func() int { matcher.MatchOutput.Err <- fmt.Errorf("some-message") matcher.MatchOutput.ResultValue <- nil return 99 } _, err := m.Match(f) if err == nil { t.Fatal("expected err to not be nil") } if err.Error() != "some-message" { t.Errorf("expected err to have message: %s", "some-message") } } onpar-0.3.3/onpar.go000066400000000000000000000250661457413705600143270ustar00rootroot00000000000000//go:generate hel package onpar import ( "errors" "fmt" "path" "testing" ) type prefs struct { } // Opt is an option type to pass to onpar's constructor. type Opt func(prefs) prefs type suite interface { Run() addRunner(runner) child() child } type child interface { addSpecs() } // Table is an entry to be used in table tests. type Table[T, U, V any] struct { parent *Onpar[T, U] spec func(U, V) } // TableSpec returns a Table type which may be used to declare table tests. The // spec argument is the test that will be run for each entry in the table. // // This is effectively syntactic sugar for looping over table tests and calling // `parent.Spec` for each entry in the table. func TableSpec[T, U, V any](parent *Onpar[T, U], spec func(U, V)) Table[T, U, V] { return Table[T, U, V]{parent: parent, spec: spec} } // Entry adds an entry to t using entry as the value for this table entry. func (t Table[T, U, V]) Entry(name string, entry V) Table[T, U, V] { t.parent.Spec(name, func(v U) { t.spec(v, entry) }) return t } // FnEntry adds an entry to t that calls setup in order to get its entry value. // The value from the BeforeEach will be passed to setup, and then both values // will be passed to the table spec. func (t Table[T, U, V]) FnEntry(name string, setup func(U) V) Table[T, U, V] { t.parent.Spec(name, func(v U) { entry := setup(v) t.spec(v, entry) }) return t } // Onpar stores the state of the specs and groups type Onpar[T, U any] struct { t TestRunner path []string parent suite // level is handled by (*Onpar[T]).Group(), which will adjust this field // each time it is called. This is how onpar knows to create nested `t.Run` // calls. level *level[T, U] // canBeforeEach controls which contexts BeforeEach is allowed to take this // suite as a parent suite. canBeforeEach bool // childSuite is assigned by BeforeEach and removed at the end of Group. If // BeforeEach is called twice in the same Group (or twice at the top level), // this is how it knows to panic. // // At the end of Group calls, childSuite.addSpecs is called, which will sync the // childSuite's specs to the parent. childSuite child childPath []string runCalled bool } // TestRunner matches the methods in *testing.T that the top level onpar // (returned from New) needs in order to work. type TestRunner interface { Run(name string, fn func(*testing.T)) bool Failed() bool Cleanup(func()) } // New creates a new Onpar suite. The top-level onpar suite must be constructed // with this. Think `context.Background()`. // // It's normal to construct the top-level suite with a BeforeEach by doing the // following: // // o := BeforeEach(New(t), setupFn) func New[T TestRunner](t T, opts ...Opt) *Onpar[*testing.T, *testing.T] { p := prefs{} for _, opt := range opts { p = opt(p) } o := Onpar[*testing.T, *testing.T]{ t: t, canBeforeEach: true, level: &level[*testing.T, *testing.T]{ before: func(t *testing.T) *testing.T { return t }, }, } t.Cleanup(func() { if t.Failed() { return } if r := recover(); r != nil { panic(r) } if !o.runCalled { panic("onpar: Run was never called [hint: missing 'defer o.Run()'?]") } }) return &o } // Run runs all of o's tests. Typically this will be called in a `defer` // immediately after o is defined: // // o := onpar.BeforeEach(onpar.New(t), setupFn) // defer o.Run() func (o *Onpar[T, U]) Run() { if o.parent == nil { o.runCalled = true o.run(o.t) return } o.parent.Run() } // BeforeEach creates a new child Onpar suite with the requested function as the // setup function for all specs. It requires a parent Onpar. // // The top level Onpar *must* have been constructed with New, otherwise the // suite will not run. // // BeforeEach should be called only once for each level (i.e. each group). It // will panic if it detects that it is overwriting another BeforeEach call for a // given level. func BeforeEach[T, U, V any](parent *Onpar[T, U], setup func(U) V) *Onpar[U, V] { if !parent.canBeforeEach { panic(fmt.Errorf("onpar: BeforeEach called with invalid parent: parent must either be a top-level suite or be used inside of a `parent.Group()` call")) } if !parent.correctGroup() { panic(fmt.Errorf("onpar: BeforeEach called with invalid parent: parent suite can only be used inside of its group (%v), but the group has exited", path.Join(parent.path...))) } if parent.child() != nil { if len(parent.childPath) == 0 { panic(errors.New("onpar: BeforeEach was called more than once at the top level")) } panic(fmt.Errorf("onpar: BeforeEach was called more than once for group '%s'", path.Join(parent.childPath...))) } path := parent.path if parent.level.name() != "" { path = append(parent.path, parent.level.name()) } child := &Onpar[U, V]{ path: path, parent: parent, level: &level[U, V]{ before: setup, }, } parent.childSuite = child parent.childPath = child.path return child } // Spec is a test that runs in parallel with other specs. func (o *Onpar[T, U]) Spec(name string, f func(U)) { if !o.correctGroup() { panic(fmt.Errorf("onpar: Spec called on child suite outside of its group (%v)", path.Join(o.path...))) } spec := concurrentSpec[U]{ serialSpec: serialSpec[U]{ specName: name, f: f, }, } o.addRunner(spec) } // SerialSpec is a test that runs synchronously (i.e. onpar will not call // `t.Parallel`). While onpar is primarily a parallel testing suite, we // recognize that sometimes a test just can't be run in parallel. When that is // the case, use SerialSpec. func (o *Onpar[T, U]) SerialSpec(name string, f func(U)) { if !o.correctGroup() { panic(fmt.Errorf("onpar: SerialSpec called on child suite outside of its group (%v)", path.Join(o.path...))) } spec := serialSpec[U]{ specName: name, f: f, } o.addRunner(spec) } func (o *Onpar[T, U]) addRunner(r runner) { o.level.runners = append(o.level.runners, r) } // Group is used to gather and categorize specs. Inside of each group, a new // child *Onpar may be constructed using BeforeEach. func (o *Onpar[T, U]) Group(name string, f func()) { if !o.correctGroup() { panic(fmt.Errorf("onpar: Group called on child suite outside of its group (%v)", path.Join(o.path...))) } oldLevel := o.level o.level = &level[T, U]{ levelName: name, } o.canBeforeEach = true defer func() { o.canBeforeEach = false if o.child() != nil { o.child().addSpecs() o.childSuite = nil } oldLevel.runners = append(oldLevel.runners, &level[U, U]{ levelName: o.level.name(), before: func(v U) U { return v }, runners: o.level.runners, }) o.level = oldLevel }() f() } // AfterEach is used to cleanup anything from the specs or BeforeEaches. // AfterEach may only be called once for each *Onpar value constructed. func (o *Onpar[T, U]) AfterEach(f func(U)) { if !o.correctGroup() { panic(fmt.Errorf("onpar: AfterEach called on child suite outside of its group (%v)", path.Join(o.path...))) } if o.level.after != nil { if len(o.childPath) == 0 { panic(errors.New("onpar: AfterEach was called more than once at top level")) } panic(fmt.Errorf("onpar: AfterEach was called more than once for group '%s'", path.Join(o.path...))) } o.level.after = f } func (o *Onpar[T, U]) run(t TestRunner) { if o.child() != nil { // This happens when New is called before BeforeEach, e.g.: // // o := onpar.New() // defer o.Run(t) // // b := onpar.BeforeEach(o, setup) // // Since there's no call to o.Group, the child won't be synced, so we // need to do that here. o.child().addSpecs() o.childSuite = nil } top, ok := any(o.level).(groupRunner[*testing.T]) if !ok { // This should be impossible - the only place that `run` is called is in // `New()`, which is only capable of returning `*Onpar[*testing.T, // *testing.T]`. var empty T panic(fmt.Errorf("onpar: run was called on a child level (type '%T' is not *testing.T)", empty)) } top.runSpecs(t, func() testScope[*testing.T] { return baseScope{} }) } func (o *Onpar[T, U]) child() child { return o.childSuite } func (o *Onpar[T, U]) correctGroup() bool { if o.parent == nil { return true } if o.parent.child() == o { return true } return false } // addSpecs is called by parent Group() calls to tell o to add its specs to its // parent. func (o *Onpar[T, U]) addSpecs() { o.parent.addRunner(o.level) } type testScope[T any] interface { before(*testing.T) T after() } type baseScope struct { } func (s baseScope) before(t *testing.T) *testing.T { return t } func (s baseScope) after() {} type runner interface { name() string } type groupRunner[T any] interface { runner runSpecs(t TestRunner, scope func() testScope[T]) } type specRunner[T any] interface { runner run(t *testing.T, scope func() testScope[T]) } type concurrentSpec[T any] struct { serialSpec[T] } func (s concurrentSpec[T]) run(t *testing.T, scope func() testScope[T]) { t.Parallel() s.serialSpec.run(t, scope) } type serialSpec[T any] struct { specName string f func(T) } func (s serialSpec[T]) name() string { return s.specName } func (s serialSpec[T]) run(t *testing.T, scope func() testScope[T]) { sc := scope() v := sc.before(t) s.f(v) sc.after() } type levelScope[T, U any] struct { val U parentBefore func(*testing.T) T childBefore func(T) U childAfter func(U) parentAfter func() } func (s *levelScope[T, U]) before(t *testing.T) U { parentVal := s.parentBefore(t) s.val = s.childBefore(parentVal) return s.val } func (s *levelScope[T, U]) after() { if s.childAfter != nil { s.childAfter(s.val) } if s.parentAfter != nil { s.parentAfter() } } type level[T, U any] struct { levelName string before func(T) U after func(U) runners []runner } func (l *level[T, U]) name() string { return l.levelName } func (l *level[T, U]) runSpecs(t TestRunner, scope func() testScope[T]) { for _, r := range l.runners { childScope := func() testScope[U] { parentScope := scope() return &levelScope[T, U]{ parentBefore: parentScope.before, childBefore: l.before, childAfter: l.after, parentAfter: parentScope.after, } } switch r := r.(type) { case groupRunner[U]: if r.name() == "" { // If the name is empty, running the group as a sub-group would // result in ugly output. Just run the test function at this level // instead. r.runSpecs(t, childScope) return } t.Run(r.name(), func(t *testing.T) { r.runSpecs(t, childScope) }) case specRunner[U]: t.Run(r.name(), func(t *testing.T) { r.run(t, childScope) }) default: panic(fmt.Errorf("onpar: spec runner type [%T] is not supported", r)) } } } onpar-0.3.3/onpar_test.go000066400000000000000000000270361457413705600153650ustar00rootroot00000000000000package onpar_test import ( "reflect" "strings" "testing" "time" "git.sr.ht/~nelsam/hel/pkg/pers" "github.com/poy/onpar" ) const testTimeout = time.Second func TestPanicsWithMissingRun(t *testing.T) { t.Parallel() mockT := newMockTestRunner(t, testTimeout) var cleanup func() seq := pers.CallSequence(t) pers.Expect(seq, mockT, "Cleanup", pers.StoreArgs(&cleanup)) onpar.New(mockT) seq.Check(t) if cleanup == nil { t.Fatalf("expected Cleanup to be called with a cleanup function") } defer func() { r := recover() if r == nil { t.Fatalf("expected onpar to panic if Run was never called") } }() pers.Expect(seq, mockT, "Failed", pers.Returning(false)) cleanup() } func TestMissingRunIsNotMentionedIfTestPanics(t *testing.T) { t.Parallel() mockT := newMockTestRunner(t, testTimeout) seq := pers.CallSequence(t) var cleanup func() pers.Expect(seq, mockT, "Cleanup", pers.StoreArgs(&cleanup)) onpar.New(mockT) seq.Check(t) defer func() { r := recover() if r == nil { t.Fatalf("expected test panic to surface") } msg, ok := r.(string) if !ok { t.Fatalf("expected test panic to be surfaced as a string") } if strings.Contains(msg, "missing 'defer o.Run()'") { t.Fatalf("did not expect onpar to mention missing o.Run when calling context panicked") } }() pers.Expect(seq, mockT, "Failed", pers.Returning(false)) defer cleanup() panic("boom") } func TestMissingRunIsNotMentionedIfTestIsFailed(t *testing.T) { t.Parallel() mockT := newMockTestRunner(t, testTimeout) seq := pers.CallSequence(t) var cleanup func() pers.Expect(seq, mockT, "Cleanup", pers.StoreArgs(&cleanup)) onpar.New(mockT) seq.Check(t) defer func() { if r := recover(); r != nil { t.Fatalf("expected onpar not to panic if the test is failed") } }() pers.Expect(seq, mockT, "Failed", pers.Returning(true)) cleanup() } func TestMissingRunIsNotMentionedWithSpecPanic(t *testing.T) { t.Parallel() mockT := newMockTestRunner(t, testTimeout) seq := pers.CallSequence(t) var cleanup func() pers.Expect(seq, mockT, "Cleanup", pers.StoreArgs(&cleanup)) pers.Panic(mockT.RunOutput, "boom") o := onpar.New(mockT) o.Spec("boom", func(t *testing.T) { panic("boom") }) seq.Check(t) defer func() { r := recover() if r == nil { t.Fatalf("expected child panic to surface") } msg, ok := r.(string) if !ok { t.Fatalf("expected child panic to be surfaced as a string") } if strings.Contains(msg, "missing 'defer o.Run()'") { t.Fatalf("did not expect onpar to mention missing o.Run when o.Run was called") } }() pers.Expect(seq, mockT, "Failed", pers.Returning(false)) defer cleanup() o.Run() } func TestSingleNestedSpec(t *testing.T) { t.Parallel() c := createScaffolding(t) objs := chanToSlice(c) if len(objs) != 4 { t.Fatalf("expected objs (len=%d) to have len %d", len(objs), 4) } objA := findSpec(objs, "DA-A") if objA == nil { t.Fatal("unable to find spec A") } if len(objA.c) != 4 { t.Fatalf("expected objs (len=%d) to have len %d", len(objA.c), 4) } if !reflect.DeepEqual(objA.c, []string{"-BeforeEach", "DA-A", "DA-AfterEach", "-AfterEach"}) { t.Fatalf("invalid call order for spec A: %v", objA.c) } } func TestInvokeFirstChildAndPeerSpec(t *testing.T) { t.Parallel() c := createScaffolding(t) objs := chanToSlice(c) objB := findSpec(objs, "DB-B") if objB == nil { t.Fatal("unable to find spec B") } if len(objB.c) != 6 { t.Fatalf("expected objs (len=%d) to have len %d", len(objB.c), 6) } if !reflect.DeepEqual(objB.c, []string{"-BeforeEach", "DB-BeforeEach", "DB-B", "DB-AfterEach", "DA-AfterEach", "-AfterEach"}) { t.Fatalf("invalid call order for spec A: %v", objB.c) } } func TestInvokeSecondChildAndPeerSpec(t *testing.T) { t.Parallel() c := createScaffolding(t) objs := chanToSlice(c) objC := findSpec(objs, "DB-C") if objC == nil { t.Fatal("unable to find spec C") } if len(objC.c) != 6 { t.Fatalf("expected objs (len=%d) to have len %d", len(objC.c), 6) } if !reflect.DeepEqual(objC.c, []string{"-BeforeEach", "DB-BeforeEach", "DB-C", "DB-AfterEach", "DA-AfterEach", "-AfterEach"}) { t.Fatalf("invalid call order for spec A: %v", objC.c) } } func TestNewWithBeforeEach(t *testing.T) { c := make(chan string, 100) t.Run("FakeSpecs", func(t *testing.T) { o := onpar.New(t) defer o.Run() o.SerialSpec("it runs a spec without a beforeeach", func(*testing.T) { c <- "A" }) b := onpar.BeforeEach(o, func(*testing.T) string { c <- "B-BeforeEach" return "foo" }) b.SerialSpec("it runs a spec on a BeforeEach", func(string) { c <- "B" }) }) expected := []string{"A", "B-BeforeEach", "B"} for len(expected) > 0 { select { case v := <-c: if v != expected[0] { t.Fatalf("expected %v, got %v", expected[0], v) return } expected = expected[1:] default: t.Fatalf("expected %v to be called but it never was", expected[0]) return } } } func TestGroupNestsRunCalls(t *testing.T) { c := make(chan string, 100) t.Run("FakeSpecs", func(t *testing.T) { o := onpar.New(t) defer o.Run() sendName := func(t *testing.T) { c <- t.Name() } o.Spec("A", sendName) o.Group("B", func() { o.Spec("C", sendName) o.Spec("D", sendName) }) o.Spec("E", sendName) o.Group("F", func() { o.Group("G", func() { o.Spec("H", sendName) }) }) }) expected := []string{ "TestGroupNestsRunCalls/FakeSpecs/A", "TestGroupNestsRunCalls/FakeSpecs/B/C", "TestGroupNestsRunCalls/FakeSpecs/B/D", "TestGroupNestsRunCalls/FakeSpecs/E", "TestGroupNestsRunCalls/FakeSpecs/F/G/H", } findMatch := func(v string) { // We aren't guaranteed order here since the specs run in parallel. for i, e := range expected { if v == e { expected = append(expected[:i], expected[i+1:]...) return } } t.Fatalf("test name %v was not expected (or was run twice)", v) } for len(expected) > 0 { select { case v := <-c: findMatch(v) default: t.Fatalf("specs %v were never called", expected) return } } } func TestSerialSpecsAreOrdered(t *testing.T) { c := make(chan string, 100) t.Run("FakeSpecs", func(t *testing.T) { o := onpar.New(t) defer o.Run() sendName := func(t *testing.T) { c <- t.Name() } o.SerialSpec("A", sendName) o.Group("B", func() { o.SerialSpec("C", sendName) o.SerialSpec("D", sendName) }) o.SerialSpec("E", sendName) o.Group("F", func() { o.Group("G", func() { o.SerialSpec("H", sendName) }) }) }) close(c) expected := []string{ "TestSerialSpecsAreOrdered/FakeSpecs/A", "TestSerialSpecsAreOrdered/FakeSpecs/B/C", "TestSerialSpecsAreOrdered/FakeSpecs/B/D", "TestSerialSpecsAreOrdered/FakeSpecs/E", "TestSerialSpecsAreOrdered/FakeSpecs/F/G/H", } i := 0 for v := range c { if i >= len(expected) { t.Fatalf("only expected %d specs, but there are %d unexpected extra calls", len(expected), len(c)) } thisExp := expected[i] if v != thisExp { t.Fatalf("expected run %d to be '%v'; got '%v'", i, thisExp, v) } i++ } if i < len(expected) { t.Fatalf("expected %d specs, but there were only %d calls", len(expected), i) } } func TestUsingSuiteOutsideGroupPanics(t *testing.T) { var r any t.Run("FakeSpecs", func(t *testing.T) { defer func() { r = recover() }() o := onpar.New(t) defer o.Run() o.Group("foo", func() { // The most likely scenario for a suite accidentally being used // outside of its group is if it is reassigned to o. This seems // unlikely, since the types usually won't match (the setup // function's parameter and return types have to exactly match the // parent suite's) - but it's worth ensuring that this panics. o = onpar.BeforeEach(o, func(t *testing.T) *testing.T { return t }) o.Spec("bar", func(*testing.T) {}) }) o.Spec("baz", func(*testing.T) {}) }) if r == nil { t.Fatalf("expected adding a spec to a *OnPar value outside of its group to panic") } } func TestUsingParentWithoutGroupPanics(t *testing.T) { var r any t.Run("FakeSpecs", func(t *testing.T) { defer func() { r = recover() }() o := onpar.New(t) defer o.Run() o.Group("foo", func() { b := onpar.BeforeEach(o, func(t *testing.T) string { return "foo" }) onpar.BeforeEach(b, func(string) int { return 1 }) }) }) if r == nil { t.Fatalf("expected creating a child suite on a parent without a group to panic") } } func createScaffolding(t *testing.T) <-chan *testObject { objs := make(chan *testObject, 100) t.Run("FakeSpecs", func(t *testing.T) { o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) mockTest { obj := NewTestObject() obj.Use("-BeforeEach") objs <- obj return mockTest{t, 99, "something", obj} }) defer o.Run() o.AfterEach(func(tt mockTest) { tt.o.Use("-AfterEach") }) o.Group("DA", func() { o := onpar.BeforeEach(o, func(tt mockTest) mockTest { return tt }) o.AfterEach(func(tt mockTest) { if tt.i != 99 { tt.t.Fatalf("expected %d = %d", tt.i, 99) } if tt.s != "something" { tt.t.Fatalf("expected %s = %s", tt.s, "something") } tt.o.Use("DA-AfterEach") tt.o.Close() }) o.Spec("A", func(tt mockTest) { if tt.i != 99 { tt.t.Fatalf("expected %d = %d", tt.i, 99) } if tt.s != "something" { tt.t.Fatalf("expected %s = %s", tt.s, "something") } tt.o.Use("DA-A") }) o.Group("DB", func() { type subMockTest struct { t *testing.T i int s string o *testObject f float64 } o := onpar.BeforeEach(o, func(tt mockTest) subMockTest { tt.o.Use("DB-BeforeEach") return subMockTest{t: tt.t, i: tt.i, s: tt.s, o: tt.o, f: 101} }) o.AfterEach(func(tt subMockTest) { tt.o.Use("DB-AfterEach") }) o.Spec("B", func(tt subMockTest) { tt.o.Use("DB-B") if tt.i != 99 { tt.t.Fatalf("expected %d = %d", tt.i, 99) } if tt.s != "something" { tt.t.Fatalf("expected %s = %s", tt.s, "something") } if tt.f != 101 { tt.t.Fatalf("expected %f = %f", tt.f, 101.0) } }) o.Spec("C", func(tt subMockTest) { tt.o.Use("DB-C") if tt.i != 99 { tt.t.Fatalf("expected %d = %d", tt.i, 99) } if tt.s != "something" { tt.t.Fatalf("expected %s = %s", tt.s, "something") } if tt.f != 101 { tt.t.Fatalf("expected %f = %f", tt.f, 101.0) } }) o.Group("DDD", func() { type subSubMockTest struct { o *testObject i int t *testing.T } o := onpar.BeforeEach(o, func(tt subMockTest) subSubMockTest { tt.o.Use("DDD-BeforeEach") return subSubMockTest{o: tt.o, i: tt.i, t: tt.t} }) o.AfterEach(func(tt subSubMockTest) { tt.o.Use("DDD-AfterEach") }) o.Spec("E", func(tt subSubMockTest) { tt.o.Use("DDD-E") if tt.i != 99 { tt.t.Fatalf("expected %d = %d", tt.i, 99) } }) }) }) }) }) return objs } func chanToSlice(c <-chan *testObject) []*testObject { var results []*testObject l := len(c) for i := 0; i < l; i++ { results = append(results, <-c) } return results } func findSpec(objs []*testObject, name string) *testObject { for _, obj := range objs { for _, specName := range obj.c { if name == specName { return obj } } } return nil } type testObject struct { c []string done bool } func NewTestObject() *testObject { return &testObject{ c: make([]string, 0), } } func (t *testObject) Use(i string) { t.c = append(t.c, i) } func (t *testObject) Close() { if t.done { panic("close() called too many times") } t.done = true } type mockTest struct { t *testing.T i int s string o *testObject } onpar-0.3.3/samples/000077500000000000000000000000001457413705600143145ustar00rootroot00000000000000onpar-0.3.3/samples/async/000077500000000000000000000000001457413705600154315ustar00rootroot00000000000000onpar-0.3.3/samples/async/async_test.go000066400000000000000000000004461457413705600201400ustar00rootroot00000000000000package async_test import ( "testing" . "github.com/poy/onpar/expect" . "github.com/poy/onpar/matchers" ) func TestChannel(t *testing.T) { c := make(chan int) go func() { for i := 0; i < 100; i++ { c <- i } }() Expect(t, c).To(ViaPolling( Chain(Receive(), Equal(50)), )) } onpar-0.3.3/samples/fibonacci/000077500000000000000000000000001457413705600162315ustar00rootroot00000000000000onpar-0.3.3/samples/fibonacci/fib.go000066400000000000000000000001651457413705600173220ustar00rootroot00000000000000package fibonacci func Fibonacci(n int) int { if n <= 1 { return 1 } return Fibonacci(n-2) + Fibonacci(n-1) } onpar-0.3.3/samples/fibonacci/fib_test.go000066400000000000000000000015711457413705600203630ustar00rootroot00000000000000package fibonacci_test import ( "testing" "github.com/poy/onpar" . "github.com/poy/onpar/expect" . "github.com/poy/onpar/matchers" "github.com/poy/onpar/samples/fibonacci" ) func TestDifferentInputs(t *testing.T) { o := onpar.New(t) defer o.Run() o.Group("when n is 0", func() { o.Spec("it returns 1", func(t *testing.T) { result := fibonacci.Fibonacci(0) Expect(t, result).To(Equal(1)) }) }) o.Group("when n is 1", func() { o.Spec("it returns 1", func(t *testing.T) { result := fibonacci.Fibonacci(1) Expect(t, result).To(Equal(1)) }) }) o.Group("when n is greater than 1", func() { o.Spec("it returns 8 for n=5", func(t *testing.T) { result := fibonacci.Fibonacci(5) Expect(t, result).To(Equal(8)) }) o.Spec("it returns 55 for n=9", func(t *testing.T) { result := fibonacci.Fibonacci(9) Expect(t, result).To(Equal(55)) }) }) } onpar-0.3.3/tabular_test.go000066400000000000000000000066361457413705600157030ustar00rootroot00000000000000package onpar_test import ( "bytes" "reflect" "testing" "github.com/poy/onpar" ) func TestTableSpec_Entry(t *testing.T) { t.Parallel() c := createTableScaffolding(t) objs := chanToSlice(c) if len(objs) != 2 { t.Fatalf("expected objs (len=%d) to have len %d", len(objs), 2) } objA := findSpec(objs, "DA-A") if objA == nil { t.Fatal("unable to find spec A") } if len(objA.c) != 3 { t.Fatalf("expected objs (len=%d) to have len %d", len(objA.c), 3) } if !reflect.DeepEqual(objA.c, []string{"-BeforeEach", "DA-A", "-AfterEach"}) { t.Fatalf("invalid call order for spec A: %v", objA.c) } } func ExampleTable_Entry() { var t *testing.T o := onpar.New(t) defer o.Run() type table struct { input string expectedOutput string } f := func(in string) string { return in + "world" } onpar.TableSpec(o, func(t *testing.T, tt table) { output := f(tt.input) if output != tt.expectedOutput { t.Fatalf("expected %v to produce %v; got %v", tt.input, tt.expectedOutput, output) } }). Entry("simple output", table{"hello", "helloworld"}). Entry("with a space", table{"hello ", "hello world"}). Entry("and a comma", table{"hello, ", "hello, world"}) } func TestTableSpec_FnEntry(t *testing.T) { t.Parallel() c := createTableScaffolding(t) objs := chanToSlice(c) if len(objs) != 2 { t.Fatalf("expected objs (len=%d) to have len %d", len(objs), 2) } objB := findSpec(objs, "DA-B") if objB == nil { t.Fatal("unable to find spec B") } if len(objB.c) != 4 { t.Fatalf("expected objs (len=%d) to have len %d", len(objB.c), 4) } if !reflect.DeepEqual(objB.c, []string{"-BeforeEach", "-TableEntrySetup", "DA-B", "-AfterEach"}) { t.Fatalf("invalid call order for spec A: %v", objB.c) } } func ExampleTable_FnEntry() { var t *testing.T o := onpar.New(t) defer o.Run() type table struct { input string expectedOutput string } f := func(in string) string { return in + "world" } onpar.TableSpec(o, func(t *testing.T, tt table) { output := f(tt.input) if output != tt.expectedOutput { t.Fatalf("expected %v to produce %v; got %v", tt.input, tt.expectedOutput, output) } }). FnEntry("simple output", func(t *testing.T) table { var buf bytes.Buffer if _, err := buf.WriteString("hello"); err != nil { t.Fatalf("expected buffer write to succeed; got %v", err) } return table{input: buf.String(), expectedOutput: "helloworld"} }) } func createTableScaffolding(t *testing.T) <-chan *testObject { objs := make(chan *testObject, 100) t.Run("FakeSpecs", func(t *testing.T) { o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) *mockTest { obj := NewTestObject() obj.Use("-BeforeEach") objs <- obj return &mockTest{t, 99, "something", obj} }) defer o.Run() o.AfterEach(func(tt *mockTest) { tt.o.Use("-AfterEach") }) type table struct { name string expected mockTest } onpar.TableSpec(o, func(tt *mockTest, tab table) { if tt.i != tab.expected.i { tt.t.Fatalf("expected %d = %d", tt.i, tab.expected.i) } if tt.s != tab.expected.s { tt.t.Fatalf("expected %s = %s", tt.s, tab.expected.s) } tt.o.Use(tab.name) }). Entry("DA-A", table{name: "DA-A", expected: mockTest{i: 99, s: "something"}}). FnEntry("DA-B", func(tt *mockTest) table { tt.i = 21 tt.s = "foo" tt.o.Use("-TableEntrySetup") return table{name: "DA-B", expected: mockTest{i: 21, s: "foo"}} }) }) return objs } onpar-0.3.3/testdata/000077500000000000000000000000001457413705600144615ustar00rootroot00000000000000onpar-0.3.3/testdata/verbose.out000066400000000000000000000021151457413705600166560ustar00rootroot00000000000000=== RUN TestNestedStructure === RUN TestNestedStructure/foo === PAUSE TestNestedStructure/foo === RUN TestNestedStructure/bar === PAUSE TestNestedStructure/bar === RUN TestNestedStructure/baz === RUN TestNestedStructure/baz/foo === PAUSE TestNestedStructure/baz/foo === RUN TestNestedStructure/baz/bar === RUN TestNestedStructure/baz/bar/foo === PAUSE TestNestedStructure/baz/bar/foo === CONT TestNestedStructure/baz/bar/foo === RUN TestNestedStructure/baz/baz === PAUSE TestNestedStructure/baz/baz === CONT TestNestedStructure/baz/foo === CONT TestNestedStructure/baz/baz === CONT TestNestedStructure/foo === CONT TestNestedStructure/bar --- PASS: TestNestedStructure (0.00s) --- PASS: TestNestedStructure/baz (0.00s) --- PASS: TestNestedStructure/baz/bar (0.00s) --- PASS: TestNestedStructure/baz/bar/foo (0.00s) --- PASS: TestNestedStructure/baz/foo (0.00s) --- PASS: TestNestedStructure/baz/baz (0.00s) --- PASS: TestNestedStructure/foo (0.00s) --- PASS: TestNestedStructure/bar (0.00s) PASS ok github.com/poy/onpar/v3 0.001s