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 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
|
package 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 "<nil>"
}
if ev.Kind() == reflect.Ptr {
return d.diff(ctx, av, ev.Elem())
}
return d.genDiff("%v", "<nil>", ev.Interface())
}
if !ev.IsValid() {
if av.Kind() == reflect.Ptr {
return d.diff(ctx, av.Elem(), ev)
}
return d.genDiff("%v", av.Interface(), "<nil>")
}
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
}
|