File: char_test.go

package info (click to toggle)
golang-github-poy-onpar 0.3.3-1.1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 456 kB
  • sloc: makefile: 3
file content (202 lines) | stat: -rw-r--r-- 6,773 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package 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
}