Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issues/60: Add ability to diff items #67

Merged
merged 8 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Most recent version is listed first.


# v0.0.18
- Add ability to diff items: https://github.com/komuw/kama/pull/67

# v0.0.17
- Add ability to dump items with circular references: https://github.com/komuw/kama/pull/66

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ go test -race ./... -count=1
5. https://github.com/alecthomas/repr
6. https://github.com/k0kubun/pp
7. https://github.com/jba/printsrc
8. https://github.com/kylelemons/godebug
82 changes: 82 additions & 0 deletions diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package diff provides basic text comparison (like Unix's diff(1)).
package kama

import (
"fmt"
"strings"
)

// Most of the code here is insipired by(or taken from):
// (a) https://github.com/rsc/diff whose license(BSD 3-Clause "New" or "Revised" License) can be found here: https://github.com/rsc/diff/blob/master/LICENSE

// diff returns a formatted diff of the two texts,
// showing the entire text and the minimum line-level
// additions and removals to turn text1 into text2.
// (That is, lines only in text1 appear with a leading -,
// and lines only in text2 appear with a leading +.)
func diff(text1, text2 string) string {
if text1 != "" && !strings.HasSuffix(text1, "\n") {
text1 += "(missing final newline)"
}
lines1 := strings.Split(text1, "\n")
lines1 = lines1[:len(lines1)-1] // remove empty string after final line
if text2 != "" && !strings.HasSuffix(text2, "\n") {
text2 += "(missing final newline)"
}
lines2 := strings.Split(text2, "\n")
lines2 = lines2[:len(lines2)-1] // remove empty string after final line

// Naive dynamic programming algorithm for edit distance.
// https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm
// dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j]
// (The reversed indices make following the minimum cost path
// visit lines in the same order as in the text.)
dist := make([][]int, len(lines1)+1)
for i := range dist {
dist[i] = make([]int, len(lines2)+1)
if i == 0 {
for j := range dist[0] {
dist[0][j] = j
}
continue
}
for j := range dist[i] {
if j == 0 {
dist[i][0] = i
continue
}
cost := dist[i][j-1] + 1
if cost > dist[i-1][j]+1 {
cost = dist[i-1][j] + 1
}
if lines1[len(lines1)-i] == lines2[len(lines2)-j] {
if cost > dist[i-1][j-1] {
cost = dist[i-1][j-1]
}
}
dist[i][j] = cost
}
}

var buf strings.Builder
i, j := len(lines1), len(lines2)
for i > 0 || j > 0 {
cost := dist[i][j]
if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] {
fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i])
i--
j--
} else if i > 0 && cost == dist[i-1][j]+1 {
fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i])
i--
} else {
fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j])
j--
}
}
return buf.String()
}
45 changes: 45 additions & 0 deletions diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kama

import (
"strings"
"testing"
)

func TestSmallDiff(t *testing.T) {
t.Parallel()

tests := []struct {
text1 string
text2 string
diff string
}{
{"a b c", "a b d e f", "a b -c +d +e +f"},
{"", "a b c", "+a +b +c"},
{"a b c", "", "-a -b -c"},
{"a b c", "d e f", "-a -b -c +d +e +f"},
{"a b c d e f", "a b d e f", "a b -c d e f"},
{"a b c e f", "a b c d e f", "a b c +d e f"},
}

for _, tt := range tests {
// Turn spaces into \n.
text1 := strings.ReplaceAll(tt.text1, " ", "\n")
if text1 != "" {
text1 += "\n"
}
text2 := strings.ReplaceAll(tt.text2, " ", "\n")
if text2 != "" {
text2 += "\n"
}
out := diff(text1, text2)
// Cut final \n, cut spaces, turn remaining \n into spaces.
out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ")
if out != tt.diff {
t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff)
}
}
}
15 changes: 15 additions & 0 deletions kama.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,18 @@ func Dir(i interface{}, c ...Config) string {
func Stackp() {
stackp()
}

// Diffp prints a formatted diff showing the minimum line-level additions and removals that would turn old into new.
func Diffp(old, new interface{}, c ...Config) {
fmt.Println(
Diff(old, new, c...),
)
}

// Diff returns a formatted diff showing the minimum line-level additions and removals that would turn old into new.
func Diff(old, new interface{}, c ...Config) string {
return diff(
Dir(old, c...),
Dir(new, c...),
)
}
40 changes: 40 additions & 0 deletions kama_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,43 @@ func TestReadmeExamples(t *testing.T) {
})
}
}

func TestDiff(t *testing.T) {
t.Parallel()

tt := []struct {
tName string
old interface{}
new interface{}
}{
{
tName: "package compress/flate",
old: "compress/flate",
new: "compress/flate",
},
{
tName: "errors",
old: "errors",
new: "github.com/pkg/errors",
},
{
tName: "http Request",
old: http.Request{Method: "GET"},
new: http.Request{Method: "POST"},
},
}

for _, v := range tt {
v := v
tName := fmt.Sprintf("TestDiff-%s", v.tName)

t.Run(tName, func(t *testing.T) {
t.Parallel()

res := Diff(v.old, v.new)

path := getDataPath(t, "kama_test.go", tName)
dealWithTestData(t, path, res)
})
}
}
29 changes: 29 additions & 0 deletions testdata/kama_test/TestDiff-errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

[
-NAME: errors
+NAME: github.com/pkg/errors
CONSTANTS: []
VARIABLES: []
FUNCTIONS: [
- As(err error, target any) bool
+ As(err error, target interface{}) bool
+ Cause(err error) error
+ Errorf(format string, args ...interface{}) error
Is(err error, target error) bool
- Join(errs ...error) error
- New(text string) error
+ New(message string) error
Unwrap(err error) error
+ WithMessage(err error, message string) error
+ WithMessagef(err error, format string, args ...interface{}) error
+ WithStack(err error) error
+ Wrap(err error, message string) error
+ Wrapf(err error, format string, args ...interface{}) error
]
-TYPES: []
+TYPES: [
+ Frame uintptr
+ (Frame) Format(s fmt.State, verb rune)
+ (Frame) MarshalText() ([]byte, error)
+ StackTrace []Frame
+ (StackTrace) Format(s fmt.State, verb rune)]
76 changes: 76 additions & 0 deletions testdata/kama_test/TestDiff-http_Request.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

[
NAME: net/http.Request
KIND: struct
SIGNATURE: [http.Request *http.Request]
FIELDS: [
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header http.Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer http.Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct {}
Response *http.Response
]
METHODS: [
AddCookie func(*http.Request, *http.Cookie)
BasicAuth func(*http.Request) (string, string, bool)
Clone func(*http.Request, context.Context) *http.Request
Context func(*http.Request) context.Context
Cookie func(*http.Request, string) (*http.Cookie, error)
Cookies func(*http.Request) []*http.Cookie
FormFile func(*http.Request, string) (multipart.File, *multipart.FileHeader, error)
FormValue func(*http.Request, string) string
MultipartReader func(*http.Request) (*multipart.Reader, error)
ParseForm func(*http.Request) error
ParseMultipartForm func(*http.Request, int64) error
PathValue func(*http.Request, string) string
PostFormValue func(*http.Request, string) string
ProtoAtLeast func(*http.Request, int, int) bool
Referer func(*http.Request) string
SetBasicAuth func(*http.Request, string, string)
SetPathValue func(*http.Request, string, string)
UserAgent func(*http.Request) string
WithContext func(*http.Request, context.Context) *http.Request
Write func(*http.Request, io.Writer) error
WriteProxy func(*http.Request, io.Writer) error
]
SNIPPET: Request{
- Method: "GET",
+ Method: "POST",
URL: *url.URL(nil),
Proto: "",
ProtoMajor: int(0),
ProtoMinor: int(0),
Header: http.Header{(nil)},
Body: io.ReadCloser nil,
GetBody: func() (io.ReadCloser, error),
ContentLength: int64(0),
TransferEncoding: []string{(nil)},
Close: false,
Host: "",
Form: url.Values{(nil)},
PostForm: url.Values{(nil)},
MultipartForm: *multipart.Form(nil),
Trailer: http.Header{(nil)},
RemoteAddr: "",
RequestURI: "",
TLS: *tls.ConnectionState(nil),
Cancel: <-chan struct {} (len=0, cap=0),
Response: *http.Response(nil),
}
]
36 changes: 36 additions & 0 deletions testdata/kama_test/TestDiff-package_compress_flate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

[
NAME: compress/flate
CONSTANTS: [
BestCompression untyped int
BestSpeed untyped int
DefaultCompression untyped int
HuffmanOnly untyped int
NoCompression untyped int
]
VARIABLES: []
FUNCTIONS: [
NewReader(r io.Reader) io.ReadCloser
NewReaderDict(r io.Reader, dict []byte) io.ReadCloser
NewWriter(w io.Writer, level int) (*Writer, error)
NewWriterDict(w io.Writer, level int, dict []byte) (*Writer, error)
]
TYPES: [
CorruptInputError int64
(CorruptInputError) Error() string
InternalError string
(InternalError) Error() string
ReadError struct
(*ReadError) Error() string
Reader interface
(Reader) Read(p []byte) (n int, err error)
(Reader) ReadByte() (byte, error)
Resetter interface
(Resetter) Reset(r io.Reader, dict []byte) error
WriteError struct
(*WriteError) Error() string
Writer struct
(*Writer) Close() error
(*Writer) Flush() error
(*Writer) Reset(dst io.Writer)
(*Writer) Write(data []byte) (n int, err error)]
Loading