diff

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 5, 2022 License: MIT Imports: 17 Imported by: 0

README

kr.dev/diff

CI status

Hello, friend! This is a Go module to print the differences between any two Go values.

diff.Test(t, t.Errorf, got, want)

There are a few ways to use this module, but this here ☝️ is probably the most common. In test code, to check whether the result of your test is what you expected.

Usage

Test for expected value in a test:

func TestFoo(t *testing.T) {
    got := ...
    want := ...
    diff.Test(t, t.Errorf, got, want)
}

Log diffs in production:

diff.Log(a, b)

Print diffs to stdout:

diff.Each(fmt.Printf, a, b)

There are also several options to change how it works if the default behavior isn't what you need. Check out the godoc at kr.dev/diff.

Design Philosophy

Here are some general guidelines I try to follow for this module:

  • make it easy to read for humans (not computers)
  • use Go-style notation for familiarity
  • favor being concise over being explicit
  • but be explicit where necessary to avoid confusion

These aren't hard rules.

For instance, although it is a goal to make the output readable to someone who's familiar with Go, I make no effort to strictly adhere to Go syntax.

Don't Rely on Code Under Test

We also avoid calling methods on the values being compared. For instance, package time defines a method Time.Equal, that tells whether two Time values are the same instant, regardless of their locations. But we don't use it or any other Equal method. Instead, we have custom comparison logic (TimeEqual) to compare Time values while ignoring their locations.

The reason for this is that you might be trying to test your Equal method! It would be really confusing if there's a bug in the code you're testing, and that causes this package to produce incorrect results. You might end up with a "passing" test because Equal returns true even when the values are different. We want to reliably show you when the values are different.

So our policy is this module doesn't call methods on the values being compared. Instead, if you need to customize how comparisons are done, you can install a transform. See Custom Comparison. Your transform is free to call methods on the values being compared, that is up to you; this module will simply not do so directly. Our hope is that if you're doing it yourself, it'll be less surprising.

Custom Comparison

Sometimes you want or need to customize how values of a given type are compared. Here's how.

Let's say you're testing code that uses temporary files with randomized names, and your result might contain values of fs.PathError. You want to check that the errors are the same, but you need to ignore the file name. By design, the name is different every time.

var ignorePath = diff.Transform(func(pe fs.PathError) any {
    pe.Path = "" // it's ok to modify this copy
    return pe
})

diff.Test(t, t.Errorf, a, b, ignorePath)

In this case, you use the Transform option to change each fs.PathError into a new value, so that the transformed values are equal as long as the other fields, Op and Err, are equal, and unequal otherwise.

There's also a shorthand notation for this pattern. This example can be rewritten using the ZeroFields helper option.

var ignorePath = diff.ZeroFields[fs.PathError]("Path")

There are also a couple of predefined transforms exported by this package. Their definitions are visible in the godoc at kr.dev/diff.

Side note. Why doesn't the option look like this?

diff.CustomCompare(equal func(a, b T) bool) Option

That's because, under the hood, we don't always directly compare two values for equality. Sometimes we also hash the values and compare their hashes. If you wanted to define a custom boolean equality function, you'd also have to provide a custom hashing function. But with a transform, this module can do both the hashing and comparison for you, and you only need to write one relatively easy function.

Custom Formatting

This package tries to provide readable and useful output out of the box. But sometimes you can do a lot better by tailoring the output to a specific type. In that case, you can use the Format option to define a custom format function.

var fmtFoo = diff.Format(func(a, b Foo) string {
    // TODO(kr): I need a good example to put here.
    // For now, look at diff.TimeDelta in the godoc.
})

diff.Test(t, t.Errorf, a, b, fmtFoo)

Your formatter takes two values of the given type and returns a description of the difference between them. It only gets called when the values are already determined to be unequal, so you don't have to compare them. You can assume they are different. (If you need to customize how values are compared, see Custom Comparison.)

There are also a couple of predefined custom formats exported by this package. Their definitions are visible in the godoc at kr.dev/diff.

Compatibility

The output of this package is mainly meant for humans to read, and it's not a goal to be easy for computers to parse. We will occasionally change the format to try to make it better. So please keep this in mind if you want to make a tool that consumes the output of this module; it might break!

On top of that, this is still a v0 module, so we might also change the API in a way that breaks.

Roadmap

No promises here, but this is what I intend to work on:

  • example tests
  • full output mode
  • sort map keys when possible
  • detect cycles when formatting full output
  • myers diff for arrays and slices
  • special handling for text (string and []byte)
  • special handling for whitespace-only diffs
  • special handling for binary (string and []byte)
  • histogram and/or patience diff algorithm
  • format single value API (package, maybe module?)
  • make depth limit configuable (as "precision")

Feedback

If you find bugs or want more features or have design feedback about the interface, please file issues or pull requests! Thank you!

Documentation

Overview

Package diff finds the differences between a pair of Go values.

The Test, Log, and Each functions all traverse their two arguments, a and b, in parallel, looking for differences. Each difference is emitted to the given testing output function, logger, or callback function.

Here are some common usage examples:

diff.Test(t, t.Errorf, got, want)
diff.Test(t, t.Fatalf, got, want)
diff.Test(t, t.Logf, got, want)

diff.Log(a, b)
diff.Log(a, b, diff.Logger(log.New(...)))

diff.Each(fmt.Printf, a, b)

Use Option values to change how it works if the default behavior isn't what you need.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Each

func Each(f func(format string, arg ...any) (int, error), a, b any, opt ...Option)

Each compares values a and b, calling f for each difference it finds. By default, its conditions for equality are like reflect.DeepEqual.

The behavior can be adjusted by supplying Option values. See Default for a complete list of default options. Values in opt apply in addition to (and override) the defaults.

Example
package main

import (
	"fmt"
	"net"
	"time"

	"kr.dev/diff"
)

func main() {
	var a, b net.Dialer
	a.Timeout = 5 * time.Second
	b.Timeout = 10 * time.Second
	b.LocalAddr = &net.TCPAddr{}
	diff.Each(fmt.Printf, a, b)
}
Output:

net.Dialer.Timeout: 5s != 10s
net.Dialer.LocalAddr: nil != &net.TCPAddr{IP:nil, ...}

func Log

func Log(a, b any, opt ...Option)

Log compares values a and b, printing each difference to its logger. By default, its logger object is log.Default() and its conditions for equality are like reflect.DeepEqual.

The logger can be set using the Logger option. The behavior can also be adjusted by supplying other Option values. See Default for a complete list of default options. Values in opt apply in addition to (and override) the defaults.

Example
package main

import (
	"log"
	"net/url"
	"os"

	"kr.dev/diff"
)

func main() {
	logger := log.New(os.Stdout, "", 0)

	reqURL, err := url.Parse("https://example.org/?q=one")
	if err != nil {
		return
	}

	knownURL := &url.URL{
		Scheme: "https",
		Host:   "example.org",
		Path:   "/",
	}

	diff.Log(reqURL, knownURL, diff.Logger(logger))
}
Output:

url.URL.RawQuery: "q=one" != ""

func Test

func Test(h Helperer, f func(format string, arg ...any), got, want any, opt ...Option)

Test compares values got and want, calling f for each difference it finds. By default, its conditions for equality are like reflect.DeepEqual.

Test also calls h.Helper() at the top of every internal function. Note that *testing.T and *testing.B satisfy this interface. This makes test output show the file and line number of the call to Test.

The behavior can be adjusted by supplying Option values. See Default for a complete list of default options. Values in opt apply in addition to (and override) the defaults.

Example
package main

import (
	"net"
	"testing"
	"time"

	"kr.dev/diff"
)

var t = new(testing.T)

func main() {
	// TestExample(t *testing.T) {
	got := makeDialer()

	want := &net.Dialer{
		Timeout:   10 * time.Second,
		LocalAddr: &net.TCPAddr{},
	}

	diff.Test(t, t.Errorf, got, want)
	// }
}

func makeDialer() *net.Dialer {
	return &net.Dialer{Timeout: 5 * time.Second}
}
Output:

Types

type Helperer

type Helperer interface {
	Helper()
}

Helperer marks the caller as a helper function. It is satisfied by *testing.T and *testing.B.

type Option

type Option struct {
	// contains filtered or unexported fields
}

Option values can be passed to the Each function to control how comparisons are made, how output is formatted, and various other things. Options are applied in order from left to right; later options win where there is a conflict.

var (
	// Default is a copy of the default options used by Each.
	// (This variable is only for documentation;
	// modifying it has no effect on the default behavior.)
	Default Option = OptionList(
		EmitAuto,
		TimeEqual,
		TimeDelta,
		Logger(log.Default()),
	)

	// Picky is a set of options for exact comparison and
	// maximum verbosity.
	Picky Option = OptionList(
		EmitFull,
		TransformRemove[time.Time](),
		FormatRemove[time.Time](),
	)
)
var (
	// EmitAuto selects an output format for each difference
	// based on various heuristics.
	// It uses registered format functions. See Format.
	EmitAuto Option = verbosity(auto)

	// EmitPathOnly outputs the path to each difference
	// in Go notation.
	// It does not use registered format functions.
	EmitPathOnly Option = verbosity(pathOnly)

	// EmitFull outputs the path to each difference
	// and a full representation of both values
	// at that position, pretty-printed on multiple
	// lines with indentation.
	EmitFull Option = verbosity(full)
)
var (
	// TimeEqual converts Time values to a form that can be compared
	// meaningfully by the == operator.
	// See the documentation on the Time type and Time.Equal
	// for an explanation.
	TimeEqual Option = Transform(func(t time.Time) any {
		return t.Round(0).UTC()
	})

	// EqualNaN causes NaN float64 values to be treated as equal.
	EqualNaN Option = Transform(func(f float64) any {
		if math.IsNaN(f) {
			type equalNaN struct{}
			return equalNaN{}
		}
		return f
	})

	// TimeDelta outputs the difference between two times
	// in a more readable format, including the delta between them.
	TimeDelta Option = Format(func(a, b time.Time) string {
		as := a.Format(time.RFC3339Nano)
		bs := b.Format(time.RFC3339Nano)
		return fmt.Sprintf("%s != %s (%s)", as, bs, b.Sub(a))
	})
)

func EqualFuncs

func EqualFuncs(b bool) Option

EqualFuncs controls how function values are compared. If true, any two non-nil function values of the same type are treated as equal; otherwise, two non-nil functions are treated as unequal, even if they point to the same location in code. Note that EqualFuncs(false) matches the behavior of the built-in == operator.

func Format

func Format[T any](f func(a, b T) string) Option

Format customizes the description of the difference between two unequal values a and b.

See FormatRemove to remove a custom format.

func FormatRemove

func FormatRemove[T any]() Option

FormatRemove removes any format for type T. See Format.

func KeepExported

func KeepExported[T any]() Option

KeepExported transforms a value of struct type T. It makes a copy of its input, preserving exported fields only.

This effectively makes comparison use only exported fields.

See also Transform.

func KeepFields

func KeepFields[T any](name ...string) Option

KeepFields transforms values of struct type T. It makes a copy of its input, preserving the named field values and setting all other fields to their zero values.

This effectively makes comparison use only the provided fields.

KeepFields panics if any name argument is not a visible field in T. See Transform for more info about transforms. See also ZeroFields.

func Logger

func Logger(out Outputter) Option

Logger sets the output for Log to the given object. It has no effect on Each or Test.

func OptionList

func OptionList(opt ...Option) Option

OptionList combines multiple options into one. The arguments will be applied in order from left to right.

func ShowOriginal added in v0.2.0

func ShowOriginal() Option

ShowOriginal show diffs of untransformed values in addition to the diffs of transformed values. This is mainly useful for debugging transform functions.

func Transform

func Transform[T any](f func(T) any) Option

Transform converts values of type T to another value to be compared.

A transform is applied when the values on both sides of the comparison are of type T. The two values returned by the transform (one on each side) are then compared, and their diffs emitted. See also ShowOriginal.

Function f may return any type, not just T. In particular, during a single comparison, f may return a different type on each side (and this will result in a difference being reported).

See TransformRemove to remove a transform.

func TransformRemove

func TransformRemove[T any]() Option

TransformRemove removes any transform for type T. See Transform.

func ZeroFields

func ZeroFields[T any](name ...string) Option

ZeroFields transforms values of struct type T. It makes a copy of its input and sets the named fields to their zero values.

This effectively makes comparison ignore the given fields.

ZeroFields panics if any name argument is not a visible field in T. See Transform for more info about transforms. See also KeepFields.

type Outputter

type Outputter interface {
	Output(calldepth int, s string) error
}

Outputter accepts log output. It is satisfied by *log.Logger.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL