2 Commits

Author SHA1 Message Date
irlndts
14a62aa3f7 update vendor 2020-06-01 10:07:19 +03:00
Artem Piskun
9e62844f82 Move to interfaces 2020-06-01 10:03:01 +03:00
41 changed files with 270 additions and 1933 deletions

View File

@@ -1,25 +0,0 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

View File

@@ -1,18 +0,0 @@
name: golangci-lint
on:
push:
tags:
- v*
branches:
- master
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.41.1

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: go
go:
- "1.14"

View File

@@ -1,28 +1,22 @@
# REST API 2.0 Discogs.com client
[![Build Status](https://travis-ci.org/irlndts/go-discogs.svg?branch=master)](https://travis-ci.org/irlndts/go-discogs)[![Go Report Card](https://goreportcard.com/badge/github.com/irlndts/go-discogs)](https://goreportcard.com/report/github.com/irlndts/go-discogs)
go-discogs is a Go client library for the [Discogs API](https://www.discogs.com/developers/). Check the usage section to see how to access the Discogs API.
The lib is under MIT but be sure you are familiar with [Discogs API Terms of Use](https://support.discogs.com/hc/en-us/articles/360009334593-API-Terms-of-Use).
### Features
### Feauteres
* Database
* [Releases](#releases)
* Release Rating
* Master Releases
* Master Versions
* Artists
* Artist Releases
* Label
* All Label Releases
* [Releases](#releases)
* Release Rating
* Master Releases
* Master Versions
* Artists
* Artist Releases
* Label
* All Label Releases
* [Search](#search)
* [User Collection](#user-collection)
* Collection Folders
* Folder
* Collection Items by Folder
* Collection Items by Release
* [Marketplace](#marketplace)
* Price Suggestions
* Release Statistics
Install
--------
@@ -36,7 +30,7 @@ First of all import library and init client variable. According to discogs api d
import "github.com/irlndts/go-discogs"
```
Some requests require authentication (as any user). According to [Discogs](https://www.discogs.com/developers/#page:authentication,header:authentication-discogs-auth-flow), to send requests with Discogs Auth, you have two options: sending your credentials in the query string with key and secret parameters or a [token parameter](https://www.discogs.com/settings/developers).
Some requests require authentification (as any user). According to [Discogs](https://www.discogs.com/developers/#page:authentication,header:authentication-discogs-auth-flow), to send requests with Discogs Auth, you have two options: sending your credentials in the query string with key and secret parameters or a [token parameter](https://www.discogs.com/settings/developers).
```go
client, err := discogs.New(&discogs.Options{
@@ -85,6 +79,7 @@ type SearchRequest struct {
}
```
Example
```go
request := discogs.SearchRequest{Artist: "reggaenauts", ReleaseTitle: "river rock", Page: 0, PerPage: 1}
search, _ := client.Search(request)
@@ -93,48 +88,3 @@ type SearchRequest struct {
fmt.Println(r.Title)
}
```
#### User Collection
Query a users [collection](https://www.discogs.com/developers#page:user-collection).
##### Collection Folders
```go
collection, err := client.CollectionFolders("my_user")
```
##### Folder
```go
folder, err := client.Folder("my_user", 0)
```
##### Collection Items by Folder
```go
items, err := client.CollectionItemsByFolder("my_user", 0, &Pagination{Sort: "artist", SortOrder: "desc", PerPage: 2})
```
##### Collection Items by Release
```go
items, err := client.CollectionItemsByRelease("my_user", 12934893)
```
#### Marketplace
Query a user's [marketplace](https://www.discogs.com/developers/#page:marketplace)
##### Price Suggestions
Retrieve price suggestions for the provided Release ID
```go
suggestions, err := client.PriceSuggestions(12345)
```
##### Release Statistics
Retrieve marketplace statistics for the provided Release ID
```go
stats, err := client.ReleaseStatisctics(12345)
```
...
by the way, this is [my discogs page](https://www.discogs.com/user/magnetic-loft-music)

View File

@@ -119,7 +119,6 @@ type Artist struct {
ResourceURL string `json:"resource_url"`
URI string `json:"uri"`
URLs []string `json:"urls"`
Groups []Member `json:"groups,omitempty"`
DataQuality string `json:"data_quality"`
}

View File

@@ -26,17 +26,13 @@ type Options struct {
// Discogs is an interface for making Discogs API requests.
type Discogs interface {
CollectionService
DatabaseService
MarketPlaceService
SearchService
}
type discogs struct {
CollectionService
DatabaseService
SearchService
MarketPlaceService
}
var header *http.Header
@@ -66,10 +62,8 @@ func New(o *Options) (Discogs, error) {
}
return discogs{
newCollectionService(o.URL + "/users"),
newDatabaseService(o.URL, cur),
newSearchService(o.URL + "/database/search"),
newMarketPlaceService(o.URL+"/marketplace", cur),
}, nil
}
@@ -105,8 +99,6 @@ func request(path string, params url.Values, resp interface{}) error {
switch response.StatusCode {
case http.StatusUnauthorized:
return ErrUnauthorized
case http.StatusTooManyRequests:
return ErrTooManyRequests
default:
return fmt.Errorf("unknown error: %s", response.Status)
}

View File

@@ -6,7 +6,6 @@ import (
const (
testUserAgent = "UnitTestClient/0.0.2"
testUsername = "test_user"
testToken = ""
)

View File

@@ -16,11 +16,7 @@ func (e *Error) Error() string {
// APIErrors
var (
ErrCurrencyNotSupported = &Error{"currency does not supported"}
ErrInvalidReleaseID = &Error{"invalid release id"}
ErrInvalidSortKey = &Error{"invalid sort key"}
ErrInvalidUsername = &Error{"invalid username"}
ErrTooManyRequests = &Error{"too many requests"}
ErrUnauthorized = &Error{"authentication required"}
ErrCurrencyNotSupported = &Error{"currency does not supported"}
ErrUserAgentInvalid = &Error{"invalid user-agent"}
)

4
go.mod
View File

@@ -1,5 +1,5 @@
module github.com/irlndts/go-discogs
go 1.17
go 1.14
require github.com/google/go-cmp v0.5.6
require github.com/google/go-cmp v0.4.1

4
go.sum
View File

@@ -1,4 +1,4 @@
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,72 +0,0 @@
package discogs
import (
"net/url"
"strconv"
)
const (
priceSuggestionsURI = "/price_suggestions/"
releaseStatsURI = "/stats/"
)
type marketPlaceService struct {
url string
currency string
}
type MarketPlaceService interface {
// The best price suggestions according to grading
// Authentication is required.
PriceSuggestions(releaseID int) (*PriceListing, error)
// Short summary of marketplace listings
// Authentication is optional.
ReleaseStatistics(releaseID int) (*Stats, error)
}
func newMarketPlaceService(url string, currency string) MarketPlaceService {
return &marketPlaceService{
url: url,
currency: currency,
}
}
// Listing is a marketplace listing with the user's currency and a price value
type Listing struct {
Currency string `json:"currency"`
Value float64 `json:"value"`
}
// PriceListings are Listings per grading quality
type PriceListing struct {
VeryGood *Listing `json:"Very Good (VG),omitempty"`
GoodPlus *Listing `json:"Good Plus (G+),omitempty"`
NearMint *Listing `json:"Near Mint (NM or M-)"`
Good *Listing `json:"Good (G),omitempty"`
VeryGoodPlus *Listing `json:"Very Good Plus (VG+),omitempty"`
Mint *Listing `json:"Mint (M),omitempty"`
Fair *Listing `json:"Fair (F),omitempty"`
Poor *Listing `json:"Poor (P),omitempty"`
}
// Stats returns the marketplace stats summary for a release containing
type Stats struct {
LowestPrice *Listing `json:"lowest_price"`
ForSale int `json:"num_for_sale"`
Blocked bool `json:"blocked_from_sale"`
}
func (s *marketPlaceService) ReleaseStatistics(releaseID int) (*Stats, error) {
params := url.Values{}
params.Set("curr_abbr", s.currency)
var stats *Stats
err := request(s.url+releaseStatsURI+strconv.Itoa(releaseID), params, &stats)
return stats, err
}
func (s *marketPlaceService) PriceSuggestions(releaseID int) (*PriceListing, error) {
var listings *PriceListing
err := request(s.url+priceSuggestionsURI+strconv.Itoa(releaseID), nil, &listings)
return listings, err
}

View File

@@ -1,76 +0,0 @@
package discogs
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
)
const testReleaseID = 9893847
func MarketplaceServer(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/marketplace" + priceSuggestionsURI + strconv.Itoa(testReleaseID):
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, priceSuggestionJson); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case "/marketplace" + releaseStatsURI + strconv.Itoa(testReleaseID):
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, releaseStatsJson); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func TestMarketplacePriceSuggestions(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(MarketplaceServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
suggestion, err := d.PriceSuggestions(testReleaseID)
if err != nil {
t.Fatalf("failed to get price suggestion: %s", err)
}
json, err := json.Marshal(suggestion)
if err != nil {
t.Fatalf("failed to marshal folder: %s", err)
}
compareJson(t, string(json), priceSuggestionJson)
}
func TestMarketplaceReleaseStatistics(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(MarketplaceServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
stats, err := d.ReleaseStatistics(testReleaseID)
if err != nil {
t.Fatalf("failed to get price suggestion: %s", err)
}
json, err := json.Marshal(stats)
if err != nil {
t.Fatalf("failed to marshal folder: %s", err)
}
compareJson(t, string(json), releaseStatsJson)
}

View File

@@ -78,7 +78,6 @@ type Format struct {
Descriptions []string `json:"descriptions"`
Name string `json:"name"`
Qty string `json:"qty"`
Text string `json:"text,omitempty"`
}
// Company ...
@@ -131,8 +130,8 @@ type Page struct {
// URLsList ...
type URLsList struct {
Last string `json:"last,omitempty"`
Next string `json:"next,omitempty"`
Last string `json:"last"`
Next string `json:"next"`
}
// Version ...
@@ -189,9 +188,7 @@ type ReleaseSource struct {
// Pagination ...
type Pagination struct {
// TODO(irlndts): validate requested Sort
Sort string // year, title, format etc
// TODO(irlndts): validate requested SortOrder
Sort string // year, title, format
SortOrder string // asc, desc
Page int
PerPage int

File diff suppressed because one or more lines are too long

View File

@@ -1,141 +0,0 @@
package discogs
import (
"strconv"
)
// CollectionService is an interface to work with collection.
type CollectionService interface {
// Retrieve a list of folders in a users collection.
// If folder_id is not 0, authentication as the collection owner is required.
CollectionFolders(username string) (*CollectionFolders, error)
// Retrieve a list of items in a folder in a users collection.
// If folderID is not 0, authentication with token is required.
CollectionItemsByFolder(username string, folderID int, pagination *Pagination) (*CollectionItems, error)
// Retrieve the users collection folders which contain a specified release.
// The releaseID must be non-zero.
CollectionItemsByRelease(username string, releaseID int) (*CollectionItems, error)
// Retrieve metadata about a folder in a users collection.
Folder(username string, folderID int) (*Folder, error)
}
type collectionService struct {
url string
}
func newCollectionService(url string) CollectionService {
return &collectionService{
url: url,
}
}
// Folder serves folder response from discogs.
type Folder struct {
ID int `json:"id"`
Count int `json:"count"`
Name string `json:"name"`
ResourceURL string `json:"resource_url"`
}
func (s *collectionService) Folder(username string, folderID int) (*Folder, error) {
if username == "" {
return nil, ErrInvalidUsername
}
var folder *Folder
err := request(s.url+"/"+username+"/collection/folders/"+strconv.Itoa(folderID), nil, &folder)
return folder, err
}
// CollectionFolders serves collection response from discogs.
type CollectionFolders struct {
Folders []Folder `json:"folders"`
}
func (s *collectionService) CollectionFolders(username string) (*CollectionFolders, error) {
if username == "" {
return nil, ErrInvalidUsername
}
var collection *CollectionFolders
err := request(s.url+"/"+username+"/collection/folders", nil, &collection)
return collection, err
}
// CollectionItemSource ...
type CollectionItemSource struct {
ID int `json:"id"`
BasicInformation BasicInformation `json:"basic_information"`
DateAdded string `json:"date_added"`
FolderID int `json:"folder_id,omitempty"`
InstanceID int `json:"instance_id"`
Notes []Note `json:"notes,omitempty"`
Rating int `json:"rating"`
}
// BasicInformation ...
type BasicInformation struct {
ID int `json:"id"`
Artists []ArtistSource `json:"artists"`
CoverImage string `json:"cover_image"`
Formats []Format `json:"formats"`
Labels []LabelSource `json:"labels"`
Genres []string `json:"genres"`
MasterID int `json:"master_id"`
MasterURL *string `json:"master_url"`
ResourceURL string `json:"resource_url"`
Styles []string `json:"styles"`
Thumb string `json:"thumb"`
Title string `json:"title"`
Year int `json:"year"`
}
// Note ...
type Note struct {
ID int `json:"field_id"`
Value string `json:"value"`
}
// CollectionItems list of items in a users collection
type CollectionItems struct {
Pagination Page `json:"pagination"`
Items []CollectionItemSource `json:"releases"`
}
// valid sort keys
// https://www.discogs.com/developers#page:user-collection,header:user-collection-collection-items-by-folder
var validItemsByFolderSort = map[string]struct{}{
"": struct{}{},
"label": struct{}{},
"artist": struct{}{},
"title": struct{}{},
"catno": struct{}{},
"format": struct{}{},
"rating": struct{}{},
"added": struct{}{},
"year": struct{}{},
}
func (s *collectionService) CollectionItemsByFolder(username string, folderID int, pagination *Pagination) (*CollectionItems, error) {
if username == "" {
return nil, ErrInvalidUsername
}
if pagination != nil {
if _, ok := validItemsByFolderSort[pagination.Sort]; !ok {
return nil, ErrInvalidSortKey
}
}
var items *CollectionItems
err := request(s.url+"/"+username+"/collection/folders/"+strconv.Itoa(folderID)+"/releases", pagination.params(), &items)
return items, err
}
func (s *collectionService) CollectionItemsByRelease(username string, releaseID int) (*CollectionItems, error) {
if username == "" {
return nil, ErrInvalidUsername
}
if releaseID == 0 {
return nil, ErrInvalidReleaseID
}
var items *CollectionItems
err := request(s.url+"/"+username+"/collection/releases/"+strconv.Itoa(releaseID), nil, &items)
return items, err
}

View File

@@ -1,174 +0,0 @@
package discogs
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
)
func CollectionServer(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/users/" + testUsername + "/collection/folders":
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, collectionJson); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case "/users/" + testUsername + "/collection/folders/0":
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, folderJson); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case "/users/" + testUsername + "/collection/folders/0/releases":
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, collectionItemsByFolderJson); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case "/users/" + testUsername + "/collection/releases/12934893":
w.WriteHeader(http.StatusOK)
if _, err := io.WriteString(w, collectionItemsByRelease); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func TestCollectionServiceFolder(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
folder, err := d.Folder(testUsername, 0)
if err != nil {
t.Fatalf("failed to get folder: %s", err)
}
json, err := json.Marshal(folder)
if err != nil {
t.Fatalf("failed to marshal folder: %s", err)
}
compareJson(t, string(json), folderJson)
}
func TestCollectionServiceCollectionFolders(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
collection, err := d.CollectionFolders(testUsername)
if err != nil {
t.Fatalf("failed to get collection: %s", err)
}
json, err := json.Marshal(collection)
if err != nil {
t.Fatalf("failed to marshal collection: %s", err)
}
compareJson(t, string(json), collectionJson)
}
func TestCollectionServiceCollectionItemsByFolder(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
items, err := d.CollectionItemsByFolder(testUsername, 0, &Pagination{Sort: "artist", SortOrder: "desc", PerPage: 2})
if err != nil {
t.Fatalf("failed to get collection items: %s", err)
}
json, err := json.Marshal(items)
if err != nil {
t.Fatalf("failed to marshal collection items: %s", err)
}
compareJson(t, string(json), collectionItemsByFolderJson)
}
func TestCollectionServiceCollectionItemsByFolderError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
_, err := d.CollectionItemsByFolder(testUsername, 0, &Pagination{Sort: "invalid"})
if err != ErrInvalidSortKey {
t.Fatalf("err got=%s; want=%s", err, ErrInvalidSortKey)
}
}
func TestCollectionServiceCollectionItemsByRelease(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
items, err := d.CollectionItemsByRelease(testUsername, 12934893)
if err != nil {
t.Fatalf("failed to get collection items: %s", err)
}
json, err := json.Marshal(items)
if err != nil {
t.Fatalf("failed to marshal collection items: %s", err)
}
compareJson(t, string(json), collectionItemsByRelease)
}
func TestCollectionServiceCollectionItemsByReleaseErrors(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(CollectionServer))
defer ts.Close()
d := initDiscogsClient(t, &Options{URL: ts.URL})
type testCase struct {
username string
releaseID int
err error
}
testCases := map[string]testCase{
"invalid username": testCase{
username: "",
releaseID: 1,
err: ErrInvalidUsername,
},
"invalid release id": testCase{
username: "test-username",
releaseID: 0,
err: ErrInvalidReleaseID,
},
}
for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
_, err := d.CollectionItemsByRelease(tc.username, tc.releaseID)
if err != tc.err {
t.Fatalf("err got=%s; want=%s", err, tc.err)
}
})
}
}

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// Package cmp determines equality of values.
//
@@ -90,52 +90,6 @@ import (
// If there is a cycle, then the pointed at values are considered equal
// only if both addresses were previously visited in the same path step.
func Equal(x, y interface{}, opts ...Option) bool {
s := newState(opts)
s.compareAny(rootStep(x, y))
return s.result.Equal()
}
// Diff returns a human-readable report of the differences between two values:
// y - x. It returns an empty string if and only if Equal returns true for the
// same input values and options.
//
// The output is displayed as a literal in pseudo-Go syntax.
// At the start of each line, a "-" prefix indicates an element removed from x,
// a "+" prefix to indicates an element added from y, and the lack of a prefix
// indicates an element common to both x and y. If possible, the output
// uses fmt.Stringer.String or error.Error methods to produce more humanly
// readable outputs. In such cases, the string is prefixed with either an
// 's' or 'e' character, respectively, to indicate that the method was called.
//
// Do not depend on this output being stable. If you need the ability to
// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
s := newState(opts)
// Optimization: If there are no other reporters, we can optimize for the
// common case where the result is equal (and thus no reported difference).
// This avoids the expensive construction of a difference tree.
if len(s.reporters) == 0 {
s.compareAny(rootStep(x, y))
if s.result.Equal() {
return ""
}
s.result = diff.Result{} // Reset results
}
r := new(defaultReporter)
s.reporters = append(s.reporters, reporter{r})
s.compareAny(rootStep(x, y))
d := r.String()
if (d == "") != s.result.Equal() {
panic("inconsistent difference and equality results")
}
return d
}
// rootStep constructs the first path step. If x and y have differing types,
// then they are stored within an empty interface type.
func rootStep(x, y interface{}) PathStep {
vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y)
@@ -158,7 +112,33 @@ func rootStep(x, y interface{}) PathStep {
t = vx.Type()
}
return &pathStep{t, vx, vy}
s := newState(opts)
s.compareAny(&pathStep{t, vx, vy})
return s.result.Equal()
}
// Diff returns a human-readable report of the differences between two values.
// It returns an empty string if and only if Equal returns true for the same
// input values and options.
//
// The output is displayed as a literal in pseudo-Go syntax.
// At the start of each line, a "-" prefix indicates an element removed from x,
// a "+" prefix to indicates an element added to y, and the lack of a prefix
// indicates an element common to both x and y. If possible, the output
// uses fmt.Stringer.String or error.Error methods to produce more humanly
// readable outputs. In such cases, the string is prefixed with either an
// 's' or 'e' character, respectively, to indicate that the method was called.
//
// Do not depend on this output being stable. If you need the ability to
// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
r := new(defaultReporter)
eq := Equal(x, y, Options(opts), Reporter(r))
d := r.String()
if (d == "") != eq {
panic("inconsistent difference and equality results")
}
return d
}
type state struct {
@@ -376,7 +356,7 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
// assuming that T is assignable to R.
// Otherwise, it returns the input value as is.
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
// TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/22143).
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143).
if !flags.AtLeastGo110 {
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
return reflect.New(t).Elem()
@@ -386,7 +366,6 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
}
func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
var addr bool
var vax, vay reflect.Value // Addressable versions of vx and vy
var mayForce, mayForceInit bool
@@ -408,7 +387,6 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
// For retrieveUnexportedField to work, the parent struct must
// be addressable. Create a new copy of the values if
// necessary to make them addressable.
addr = vx.CanAddr() || vy.CanAddr()
vax = makeAddressable(vx)
vay = makeAddressable(vy)
}
@@ -419,7 +397,6 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
mayForceInit = true
}
step.mayForce = mayForce
step.paddr = addr
step.pvx = vax
step.pvy = vay
step.field = t.Field(i)

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// +build purego
@@ -10,6 +10,6 @@ import "reflect"
const supportExporters = false
func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value {
func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value {
panic("no support for forcibly accessing unexported fields")
}

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// +build !purego
@@ -17,19 +17,9 @@ const supportExporters = true
// a struct such that the value has read-write permissions.
//
// The parent struct, v, must be addressable, while f must be a StructField
// describing the field to retrieve. If addr is false,
// then the returned value will be shallowed copied to be non-addressable.
func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value {
ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
if !addr {
// A field is addressable if and only if the struct is addressable.
// If the original parent value was not addressable, shallow copy the
// value to make it non-addressable to avoid leaking an implementation
// detail of how forcibly exporting a field works.
if ve.Kind() == reflect.Interface && ve.IsNil() {
return reflect.Zero(f.Type)
}
return reflect.ValueOf(ve.Interface()).Convert(f.Type)
}
return ve
// describing the field to retrieve.
func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value {
// See https://github.com/google/go-cmp/issues/167 for discussion of the
// following expression.
return reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
}

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// +build !cmp_debug

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// +build cmp_debug

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// Package diff implements an algorithm for producing edit-scripts.
// The edit-script is a sequence of operations needed to transform one list
@@ -12,13 +12,6 @@
// is more important than obtaining a minimal Levenshtein distance.
package diff
import (
"math/rand"
"time"
"github.com/google/go-cmp/cmp/internal/flags"
)
// EditType represents a single operation within an edit-script.
type EditType uint8
@@ -119,8 +112,6 @@ func (r Result) Similar() bool {
return r.NumSame+1 >= r.NumDiff
}
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
// Difference reports whether two lists of lengths nx and ny are equal
// given the definition of equality provided as f.
//
@@ -186,11 +177,6 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
// approximately the square-root of the search budget.
searchBudget := 4 * (nx + ny) // O(n)
// Running the tests with the "cmp_debug" build tag prints a visualization
// of the algorithm running in real-time. This is educational for
// understanding how the algorithm works. See debug_enable.go.
f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
// The algorithm below is a greedy, meet-in-the-middle algorithm for
// computing sub-optimal edit-scripts between two lists.
//
@@ -208,26 +194,20 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
// frontier towards the opposite corner.
// • This algorithm terminates when either the X coordinates or the
// Y coordinates of the forward and reverse frontier points ever intersect.
//
// This algorithm is correct even if searching only in the forward direction
// or in the reverse direction. We do both because it is commonly observed
// that two lists commonly differ because elements were added to the front
// or end of the other list.
//
// Non-deterministically start with either the forward or reverse direction
// to introduce some deliberate instability so that we have the flexibility
// to change this algorithm in the future.
if flags.Deterministic || randBool {
goto forwardSearch
} else {
goto reverseSearch
}
forwardSearch:
{
// Running the tests with the "cmp_debug" build tag prints a visualization
// of the algorithm running in real-time. This is educational for
// understanding how the algorithm works. See debug_enable.go.
f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
for {
// Forward search from the beginning.
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
goto finishSearch
break
}
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
// Search in a diagonal pattern for a match.
@@ -262,14 +242,10 @@ forwardSearch:
} else {
fwdFrontier.Y++
}
goto reverseSearch
}
reverseSearch:
{
// Reverse search from the end.
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
goto finishSearch
break
}
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
// Search in a diagonal pattern for a match.
@@ -304,10 +280,8 @@ reverseSearch:
} else {
revFrontier.Y--
}
goto forwardSearch
}
finishSearch:
// Join the forward and reverse paths and then append the reverse path.
fwdPath.connect(revPath.point, f)
for i := len(revPath.es) - 1; i >= 0; i-- {

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
package flags

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
// +build !go1.10

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
// +build go1.10

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
// Package function provides functionality for identifying function types.
package function

View File

@@ -1,157 +0,0 @@
// Copyright 2020, 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 value
import (
"reflect"
"strconv"
)
// TypeString is nearly identical to reflect.Type.String,
// but has an additional option to specify that full type names be used.
func TypeString(t reflect.Type, qualified bool) string {
return string(appendTypeName(nil, t, qualified, false))
}
func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte {
// BUG: Go reflection provides no way to disambiguate two named types
// of the same name and within the same package,
// but declared within the namespace of different functions.
// Named type.
if t.Name() != "" {
if qualified && t.PkgPath() != "" {
b = append(b, '"')
b = append(b, t.PkgPath()...)
b = append(b, '"')
b = append(b, '.')
b = append(b, t.Name()...)
} else {
b = append(b, t.String()...)
}
return b
}
// Unnamed type.
switch k := t.Kind(); k {
case reflect.Bool, reflect.String, reflect.UnsafePointer,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
b = append(b, k.String()...)
case reflect.Chan:
if t.ChanDir() == reflect.RecvDir {
b = append(b, "<-"...)
}
b = append(b, "chan"...)
if t.ChanDir() == reflect.SendDir {
b = append(b, "<-"...)
}
b = append(b, ' ')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Func:
if !elideFunc {
b = append(b, "func"...)
}
b = append(b, '(')
for i := 0; i < t.NumIn(); i++ {
if i > 0 {
b = append(b, ", "...)
}
if i == t.NumIn()-1 && t.IsVariadic() {
b = append(b, "..."...)
b = appendTypeName(b, t.In(i).Elem(), qualified, false)
} else {
b = appendTypeName(b, t.In(i), qualified, false)
}
}
b = append(b, ')')
switch t.NumOut() {
case 0:
// Do nothing
case 1:
b = append(b, ' ')
b = appendTypeName(b, t.Out(0), qualified, false)
default:
b = append(b, " ("...)
for i := 0; i < t.NumOut(); i++ {
if i > 0 {
b = append(b, ", "...)
}
b = appendTypeName(b, t.Out(i), qualified, false)
}
b = append(b, ')')
}
case reflect.Struct:
b = append(b, "struct{ "...)
for i := 0; i < t.NumField(); i++ {
if i > 0 {
b = append(b, "; "...)
}
sf := t.Field(i)
if !sf.Anonymous {
if qualified && sf.PkgPath != "" {
b = append(b, '"')
b = append(b, sf.PkgPath...)
b = append(b, '"')
b = append(b, '.')
}
b = append(b, sf.Name...)
b = append(b, ' ')
}
b = appendTypeName(b, sf.Type, qualified, false)
if sf.Tag != "" {
b = append(b, ' ')
b = strconv.AppendQuote(b, string(sf.Tag))
}
}
if b[len(b)-1] == ' ' {
b = b[:len(b)-1]
} else {
b = append(b, ' ')
}
b = append(b, '}')
case reflect.Slice, reflect.Array:
b = append(b, '[')
if k == reflect.Array {
b = strconv.AppendUint(b, uint64(t.Len()), 10)
}
b = append(b, ']')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Map:
b = append(b, "map["...)
b = appendTypeName(b, t.Key(), qualified, false)
b = append(b, ']')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Ptr:
b = append(b, '*')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Interface:
b = append(b, "interface{ "...)
for i := 0; i < t.NumMethod(); i++ {
if i > 0 {
b = append(b, "; "...)
}
m := t.Method(i)
if qualified && m.PkgPath != "" {
b = append(b, '"')
b = append(b, m.PkgPath...)
b = append(b, '"')
b = append(b, '.')
}
b = append(b, m.Name...)
b = appendTypeName(b, m.Type, qualified, true)
}
if b[len(b)-1] == ' ' {
b = b[:len(b)-1]
} else {
b = append(b, ' ')
}
b = append(b, '}')
default:
panic("invalid kind: " + k.String())
}
return b
}

View File

@@ -1,6 +1,6 @@
// Copyright 2018, 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.
// license that can be found in the LICENSE.md file.
// +build purego
@@ -21,13 +21,3 @@ func PointerOf(v reflect.Value) Pointer {
// assumes that the GC implementation does not use a moving collector.
return Pointer{v.Pointer(), v.Type()}
}
// IsNil reports whether the pointer is nil.
func (p Pointer) IsNil() bool {
return p.p == 0
}
// Uintptr returns the pointer as a uintptr.
func (p Pointer) Uintptr() uintptr {
return p.p
}

View File

@@ -1,6 +1,6 @@
// Copyright 2018, 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.
// license that can be found in the LICENSE.md file.
// +build !purego
@@ -24,13 +24,3 @@ func PointerOf(v reflect.Value) Pointer {
// which is necessary if the GC ever uses a moving collector.
return Pointer{unsafe.Pointer(v.Pointer()), v.Type()}
}
// IsNil reports whether the pointer is nil.
func (p Pointer) IsNil() bool {
return p.p == nil
}
// Uintptr returns the pointer as a uintptr.
func (p Pointer) Uintptr() uintptr {
return uintptr(p.p)
}

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
package value

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
package value

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
package cmp
@@ -225,14 +225,11 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
// Unable to Interface implies unexported field without visibility access.
if !vx.CanInterface() || !vy.CanInterface() {
help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported"
const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported"
var name string
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
// Named type with unexported fields.
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
if _, ok := reflect.New(t).Interface().(error); ok {
help = "consider using cmpopts.EquateErrors to compare error values"
}
} else {
// Unnamed type with unexported fields. Derive PkgPath from field.
var pkgPath string

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
package cmp
@@ -177,8 +177,7 @@ type structField struct {
// pvx, pvy, and field are only valid if unexported is true.
unexported bool
mayForce bool // Forcibly allow visibility
paddr bool // Was parent addressable?
pvx, pvy reflect.Value // Parent values (always addressible)
pvx, pvy reflect.Value // Parent values
field reflect.StructField // Field information
}
@@ -190,8 +189,8 @@ func (sf StructField) Values() (vx, vy reflect.Value) {
// Forcibly obtain read-write access to an unexported struct field.
if sf.mayForce {
vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr)
vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr)
vx = retrieveUnexportedField(sf.pvx, sf.field)
vy = retrieveUnexportedField(sf.pvy, sf.field)
return vx, vy // CanInterface reports true
}
return sf.vx, sf.vy // CanInterface reports false
@@ -315,7 +314,7 @@ func (tf Transform) Option() Option { return tf.trans }
// pops the address from the stack. Thus, when traversing into a pointer from
// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles
// by checking whether the pointer has already been visited. The cycle detection
// uses a separate stack for the x and y values.
// uses a seperate stack for the x and y values.
//
// If a cycle is detected we need to determine whether the two pointers
// should be considered equal. The definition of equality chosen by Equal

View File

@@ -1,6 +1,6 @@
// Copyright 2017, 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.
// license that can be found in the LICENSE.md file.
package cmp
@@ -41,10 +41,7 @@ func (r *defaultReporter) String() string {
if r.root.NumDiff == 0 {
return ""
}
ptrs := new(pointerReferences)
text := formatOptions{}.FormatDiff(r.root, ptrs)
resolveReferences(text)
return text.String()
return formatOptions{}.FormatDiff(r.root).String()
}
func assert(ok bool) {

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
package cmp
@@ -11,6 +11,14 @@ import (
"github.com/google/go-cmp/cmp/internal/value"
)
// TODO: Enforce limits?
// * Enforce maximum number of records to print per node?
// * Enforce maximum size in bytes allowed?
// * As a heuristic, use less verbosity for equal nodes than unequal nodes.
// TODO: Enforce unique outputs?
// * Avoid Stringer methods if it results in same output?
// * Print pointer address if outputs still equal?
// numContextRecords is the number of surrounding equal records to print.
const numContextRecords = 2
@@ -63,56 +71,19 @@ func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
opts.TypeMode = t
return opts
}
func (opts formatOptions) WithVerbosity(level int) formatOptions {
opts.VerbosityLevel = level
opts.LimitVerbosity = true
return opts
}
func (opts formatOptions) verbosity() uint {
switch {
case opts.VerbosityLevel < 0:
return 0
case opts.VerbosityLevel > 16:
return 16 // some reasonable maximum to avoid shift overflow
default:
return uint(opts.VerbosityLevel)
}
}
const maxVerbosityPreset = 6
// verbosityPreset modifies the verbosity settings given an index
// between 0 and maxVerbosityPreset, inclusive.
func verbosityPreset(opts formatOptions, i int) formatOptions {
opts.VerbosityLevel = int(opts.verbosity()) + 2*i
if i > 0 {
opts.AvoidStringer = true
}
if i >= maxVerbosityPreset {
opts.PrintAddresses = true
opts.QualifiedNames = true
}
return opts
}
// FormatDiff converts a valueNode tree into a textNode tree, where the later
// is a textual representation of the differences detected in the former.
func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
if opts.DiffMode == diffIdentical {
opts = opts.WithVerbosity(1)
} else if opts.verbosity() < 3 {
opts = opts.WithVerbosity(3)
}
func (opts formatOptions) FormatDiff(v *valueNode) textNode {
// Check whether we have specialized formatting for this node.
// This is not necessary, but helpful for producing more readable outputs.
if opts.CanFormatDiffSlice(v) {
return opts.FormatDiffSlice(v)
}
var parentKind reflect.Kind
if v.parent != nil && v.parent.TransformerName == "" {
parentKind = v.parent.Type.Kind()
var withinSlice bool
if v.parent != nil && (v.parent.Type.Kind() == reflect.Slice || v.parent.Type.Kind() == reflect.Array) {
withinSlice = true
}
// For leaf nodes, format the value based on the reflect.Values alone.
@@ -121,8 +92,8 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out
case diffUnknown, diffIdentical:
// Format Equal.
if v.NumDiff == 0 {
outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
outx := opts.FormatValue(v.ValueX, withinSlice, visitedPointers{})
outy := opts.FormatValue(v.ValueY, withinSlice, visitedPointers{})
if v.NumIgnored > 0 && v.NumSame == 0 {
return textEllipsis
} else if outx.Len() < outy.Len() {
@@ -135,13 +106,8 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out
// Format unequal.
assert(opts.DiffMode == diffUnknown)
var list textList
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
}
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, withinSlice, visitedPointers{})
outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, withinSlice, visitedPointers{})
if outx != nil {
list = append(list, textRecord{Diff: '-', Value: outx})
}
@@ -150,57 +116,34 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out
}
return opts.WithTypeMode(emitType).FormatType(v.Type, list)
case diffRemoved:
return opts.FormatValue(v.ValueX, parentKind, ptrs)
return opts.FormatValue(v.ValueX, withinSlice, visitedPointers{})
case diffInserted:
return opts.FormatValue(v.ValueY, parentKind, ptrs)
return opts.FormatValue(v.ValueY, withinSlice, visitedPointers{})
default:
panic("invalid diff mode")
}
}
// Register slice element to support cycle detection.
if parentKind == reflect.Slice {
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
defer ptrs.Pop()
defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
}
// Descend into the child value node.
if v.TransformerName != "" {
out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
return opts.FormatType(v.Type, out)
} else {
switch k := v.Type.Kind(); k {
case reflect.Struct, reflect.Array, reflect.Slice:
out = opts.formatDiffList(v.Records, k, ptrs)
out = opts.FormatType(v.Type, out)
case reflect.Map:
// Register map to support cycle detection.
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
defer ptrs.Pop()
out = opts.formatDiffList(v.Records, k, ptrs)
out = wrapTrunkReferences(ptrRefs, out)
out = opts.FormatType(v.Type, out)
case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map:
return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k))
case reflect.Ptr:
// Register pointer to support cycle detection.
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
defer ptrs.Pop()
out = opts.FormatDiff(v.Value, ptrs)
out = wrapTrunkReferences(ptrRefs, out)
out = &textWrap{Prefix: "&", Value: out}
return textWrap{"&", opts.FormatDiff(v.Value), ""}
case reflect.Interface:
out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
return opts.WithTypeMode(emitType).FormatDiff(v.Value)
default:
panic(fmt.Sprintf("%v cannot have children", k))
}
return out
}
}
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
// Derive record name based on the data structure kind.
var name string
var formatKey func(reflect.Value) string
@@ -216,17 +159,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
case reflect.Map:
name = "entry"
opts = opts.WithTypeMode(elideType)
formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
}
maxLen := -1
if opts.LimitVerbosity {
if opts.DiffMode == diffIdentical {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
} else {
maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
}
opts.VerbosityLevel--
formatKey = formatMapKey
}
// Handle unification.
@@ -235,11 +168,6 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
var list textList
var deferredEllipsis bool // Add final "..." to indicate records were dropped
for _, r := range recs {
if len(list) == maxLen {
deferredEllipsis = true
break
}
// Elide struct fields that are zero value.
if k == reflect.Struct {
var isZero bool
@@ -263,31 +191,23 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
}
continue
}
if out := opts.FormatDiff(r.Value, ptrs); out != nil {
if out := opts.FormatDiff(r.Value); out != nil {
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
}
}
if deferredEllipsis {
list.AppendEllipsis(diffStats{})
}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
return textWrap{"{", list, "}"}
case diffUnknown:
default:
panic("invalid diff mode")
}
// Handle differencing.
var numDiffs int
var list textList
var keys []reflect.Value // invariant: len(list) == len(keys)
groups := coalesceAdjacentRecords(name, recs)
maxGroup := diffStats{Name: name}
for i, ds := range groups {
if maxLen >= 0 && numDiffs >= maxLen {
maxGroup = maxGroup.Append(ds)
continue
}
// Handle equal records.
if ds.NumDiff() == 0 {
// Compute the number of leading and trailing records to print.
@@ -311,21 +231,16 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
// Format the equal values.
for _, r := range recs[:numLo] {
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
if numEqual > numLo+numHi {
ds.NumIdentical -= numLo + numHi
list.AppendEllipsis(ds)
for len(keys) < len(list) {
keys = append(keys, reflect.Value{})
}
}
for _, r := range recs[numEqual-numHi : numEqual] {
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
recs = recs[numEqual:]
continue
@@ -337,70 +252,24 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
case opts.CanFormatDiffSlice(r.Value):
out := opts.FormatDiffSlice(r.Value)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
case r.Value.NumChildren == r.Value.MaxDepth:
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
opts2 := verbosityPreset(opts, i)
outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
}
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
if outx != nil {
list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
keys = append(keys, r.Key)
}
if outy != nil {
list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
keys = append(keys, r.Key)
}
default:
out := opts.FormatDiff(r.Value, ptrs)
out := opts.FormatDiff(r.Value)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
}
recs = recs[ds.NumDiff():]
numDiffs += ds.NumDiff()
}
if maxGroup.IsZero() {
assert(len(recs) == 0)
} else {
list.AppendEllipsis(maxGroup)
for len(keys) < len(list) {
keys = append(keys, reflect.Value{})
}
}
assert(len(list) == len(keys))
// For maps, the default formatting logic uses fmt.Stringer which may
// produce ambiguous output. Avoid calling String to disambiguate.
if k == reflect.Map {
var ambiguous bool
seenKeys := map[string]reflect.Value{}
for i, currKey := range keys {
if currKey.IsValid() {
strKey := list[i].Key
prevKey, seen := seenKeys[strKey]
if seen && prevKey.CanInterface() && currKey.CanInterface() {
ambiguous = prevKey.Interface() != currKey.Interface()
if ambiguous {
break
}
}
seenKeys[strKey] = currKey
}
}
if ambiguous {
for i, k := range keys {
if k.IsValid() {
list[i].Key = formatMapKey(k, true, ptrs)
}
}
}
}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
assert(len(recs) == 0)
return textWrap{"{", list, "}"}
}
// coalesceAdjacentRecords coalesces the list of records into groups of

View File

@@ -1,264 +0,0 @@
// Copyright 2020, 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 cmp
import (
"fmt"
"reflect"
"strings"
"github.com/google/go-cmp/cmp/internal/flags"
"github.com/google/go-cmp/cmp/internal/value"
)
const (
pointerDelimPrefix = "⟪"
pointerDelimSuffix = "⟫"
)
// formatPointer prints the address of the pointer.
func formatPointer(p value.Pointer, withDelims bool) string {
v := p.Uintptr()
if flags.Deterministic {
v = 0xdeadf00f // Only used for stable testing purposes
}
if withDelims {
return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix
}
return formatHex(uint64(v))
}
// pointerReferences is a stack of pointers visited so far.
type pointerReferences [][2]value.Pointer
func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) {
if deref && vx.IsValid() {
vx = vx.Addr()
}
if deref && vy.IsValid() {
vy = vy.Addr()
}
switch d {
case diffUnknown, diffIdentical:
pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)}
case diffRemoved:
pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}}
case diffInserted:
pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)}
}
*ps = append(*ps, pp)
return pp
}
func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) {
p = value.PointerOf(v)
for _, pp := range *ps {
if p == pp[0] || p == pp[1] {
return p, true
}
}
*ps = append(*ps, [2]value.Pointer{p, p})
return p, false
}
func (ps *pointerReferences) Pop() {
*ps = (*ps)[:len(*ps)-1]
}
// trunkReferences is metadata for a textNode indicating that the sub-tree
// represents the value for either pointer in a pair of references.
type trunkReferences struct{ pp [2]value.Pointer }
// trunkReference is metadata for a textNode indicating that the sub-tree
// represents the value for the given pointer reference.
type trunkReference struct{ p value.Pointer }
// leafReference is metadata for a textNode indicating that the value is
// truncated as it refers to another part of the tree (i.e., a trunk).
type leafReference struct{ p value.Pointer }
func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode {
switch {
case pp[0].IsNil():
return &textWrap{Value: s, Metadata: trunkReference{pp[1]}}
case pp[1].IsNil():
return &textWrap{Value: s, Metadata: trunkReference{pp[0]}}
case pp[0] == pp[1]:
return &textWrap{Value: s, Metadata: trunkReference{pp[0]}}
default:
return &textWrap{Value: s, Metadata: trunkReferences{pp}}
}
}
func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode {
var prefix string
if printAddress {
prefix = formatPointer(p, true)
}
return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}}
}
func makeLeafReference(p value.Pointer, printAddress bool) textNode {
out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"}
var prefix string
if printAddress {
prefix = formatPointer(p, true)
}
return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}}
}
// resolveReferences walks the textNode tree searching for any leaf reference
// metadata and resolves each against the corresponding trunk references.
// Since pointer addresses in memory are not particularly readable to the user,
// it replaces each pointer value with an arbitrary and unique reference ID.
func resolveReferences(s textNode) {
var walkNodes func(textNode, func(textNode))
walkNodes = func(s textNode, f func(textNode)) {
f(s)
switch s := s.(type) {
case *textWrap:
walkNodes(s.Value, f)
case textList:
for _, r := range s {
walkNodes(r.Value, f)
}
}
}
// Collect all trunks and leaves with reference metadata.
var trunks, leaves []*textWrap
walkNodes(s, func(s textNode) {
if s, ok := s.(*textWrap); ok {
switch s.Metadata.(type) {
case leafReference:
leaves = append(leaves, s)
case trunkReference, trunkReferences:
trunks = append(trunks, s)
}
}
})
// No leaf references to resolve.
if len(leaves) == 0 {
return
}
// Collect the set of all leaf references to resolve.
leafPtrs := make(map[value.Pointer]bool)
for _, leaf := range leaves {
leafPtrs[leaf.Metadata.(leafReference).p] = true
}
// Collect the set of trunk pointers that are always paired together.
// This allows us to assign a single ID to both pointers for brevity.
// If a pointer in a pair ever occurs by itself or as a different pair,
// then the pair is broken.
pairedTrunkPtrs := make(map[value.Pointer]value.Pointer)
unpair := func(p value.Pointer) {
if !pairedTrunkPtrs[p].IsNil() {
pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half
}
pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half
}
for _, trunk := range trunks {
switch p := trunk.Metadata.(type) {
case trunkReference:
unpair(p.p) // standalone pointer cannot be part of a pair
case trunkReferences:
p0, ok0 := pairedTrunkPtrs[p.pp[0]]
p1, ok1 := pairedTrunkPtrs[p.pp[1]]
switch {
case !ok0 && !ok1:
// Register the newly seen pair.
pairedTrunkPtrs[p.pp[0]] = p.pp[1]
pairedTrunkPtrs[p.pp[1]] = p.pp[0]
case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]:
// Exact pair already seen; do nothing.
default:
// Pair conflicts with some other pair; break all pairs.
unpair(p.pp[0])
unpair(p.pp[1])
}
}
}
// Correlate each pointer referenced by leaves to a unique identifier,
// and print the IDs for each trunk that matches those pointers.
var nextID uint
ptrIDs := make(map[value.Pointer]uint)
newID := func() uint {
id := nextID
nextID++
return id
}
for _, trunk := range trunks {
switch p := trunk.Metadata.(type) {
case trunkReference:
if print := leafPtrs[p.p]; print {
id, ok := ptrIDs[p.p]
if !ok {
id = newID()
ptrIDs[p.p] = id
}
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id))
}
case trunkReferences:
print0 := leafPtrs[p.pp[0]]
print1 := leafPtrs[p.pp[1]]
if print0 || print1 {
id0, ok0 := ptrIDs[p.pp[0]]
id1, ok1 := ptrIDs[p.pp[1]]
isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0]
if isPair {
var id uint
assert(ok0 == ok1) // must be seen together or not at all
if ok0 {
assert(id0 == id1) // must have the same ID
id = id0
} else {
id = newID()
ptrIDs[p.pp[0]] = id
ptrIDs[p.pp[1]] = id
}
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id))
} else {
if print0 && !ok0 {
id0 = newID()
ptrIDs[p.pp[0]] = id0
}
if print1 && !ok1 {
id1 = newID()
ptrIDs[p.pp[1]] = id1
}
switch {
case print0 && print1:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1))
case print0:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0))
case print1:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1))
}
}
}
}
}
// Update all leaf references with the unique identifier.
for _, leaf := range leaves {
if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok {
leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id))
}
}
}
func formatReference(id uint) string {
return fmt.Sprintf("ref#%d", id)
}
func updateReferencePrefix(prefix, ref string) string {
if prefix == "" {
return pointerDelimPrefix + ref + pointerDelimSuffix
}
suffix := strings.TrimPrefix(prefix, pointerDelimPrefix)
return pointerDelimPrefix + ref + ": " + suffix
}

View File

@@ -1,18 +1,17 @@
// 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.
// license that can be found in the LICENSE.md file.
package cmp
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/google/go-cmp/cmp/internal/flags"
"github.com/google/go-cmp/cmp/internal/value"
)
@@ -21,22 +20,14 @@ type formatValueOptions struct {
// methods like error.Error or fmt.Stringer.String.
AvoidStringer bool
// ShallowPointers controls whether to avoid descending into pointers.
// Useful when printing map keys, where pointer comparison is performed
// on the pointer address rather than the pointed-at value.
ShallowPointers bool
// PrintAddresses controls whether to print the address of all pointers,
// slice elements, and maps.
PrintAddresses bool
// QualifiedNames controls whether FormatType uses the fully qualified name
// (including the full package path as opposed to just the package name).
QualifiedNames bool
// VerbosityLevel controls the amount of output to produce.
// A higher value produces more output. A value of zero or lower produces
// no output (represented using an ellipsis).
// If LimitVerbosity is false, then the level is treated as infinite.
VerbosityLevel int
// LimitVerbosity specifies that formatting should respect VerbosityLevel.
LimitVerbosity bool
}
// FormatType prints the type as if it were wrapping s.
@@ -53,15 +44,12 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
default:
return s
}
if opts.DiffMode == diffIdentical {
return s // elide type for identical nodes
}
case elideType:
return s
}
// Determine the type label, applying special handling for unnamed types.
typeName := value.TypeString(t, opts.QualifiedNames)
typeName := t.String()
if t.Name() == "" {
// According to Go grammar, certain type literals contain symbols that
// do not strongly bind to the next lexicographical token (e.g., *T).
@@ -69,77 +57,39 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
case reflect.Chan, reflect.Func, reflect.Ptr:
typeName = "(" + typeName + ")"
}
typeName = strings.Replace(typeName, "struct {", "struct{", -1)
typeName = strings.Replace(typeName, "interface {", "interface{", -1)
}
return &textWrap{Prefix: typeName, Value: wrapParens(s)}
}
// wrapParens wraps s with a set of parenthesis, but avoids it if the
// wrapped node itself is already surrounded by a pair of parenthesis or braces.
// It handles unwrapping one level of pointer-reference nodes.
func wrapParens(s textNode) textNode {
var refNode *textWrap
if s2, ok := s.(*textWrap); ok {
// Unwrap a single pointer reference node.
switch s2.Metadata.(type) {
case leafReference, trunkReference, trunkReferences:
refNode = s2
if s3, ok := refNode.Value.(*textWrap); ok {
s2 = s3
}
}
// Already has delimiters that make parenthesis unnecessary.
hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")")
hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}")
// Avoid wrap the value in parenthesis if unnecessary.
if s, ok := s.(textWrap); ok {
hasParens := strings.HasPrefix(s.Prefix, "(") && strings.HasSuffix(s.Suffix, ")")
hasBraces := strings.HasPrefix(s.Prefix, "{") && strings.HasSuffix(s.Suffix, "}")
if hasParens || hasBraces {
return s
return textWrap{typeName, s, ""}
}
}
if refNode != nil {
refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"}
return s
}
return &textWrap{Prefix: "(", Value: s, Suffix: ")"}
return textWrap{typeName + "(", s, ")"}
}
// FormatValue prints the reflect.Value, taking extra care to avoid descending
// into pointers already in ptrs. As pointers are visited, ptrs is also updated.
func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) {
// into pointers already in m. As pointers are visited, m is also updated.
func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visitedPointers) (out textNode) {
if !v.IsValid() {
return nil
}
t := v.Type()
// Check slice element for cycles.
if parentKind == reflect.Slice {
ptrRef, visited := ptrs.Push(v.Addr())
if visited {
return makeLeafReference(ptrRef, false)
}
defer ptrs.Pop()
defer func() { out = wrapTrunkReference(ptrRef, false, out) }()
}
// Check whether there is an Error or String method to call.
if !opts.AvoidStringer && v.CanInterface() {
// Avoid calling Error or String methods on nil receivers since many
// implementations crash when doing so.
if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
var prefix, strVal string
func() {
// Swallow and ignore any panics from String or Error.
defer func() { recover() }()
switch v := v.Interface().(type) {
case error:
strVal = v.Error()
prefix = "e"
case fmt.Stringer:
strVal = v.String()
prefix = "s"
}
}()
if prefix != "" {
return opts.formatString(prefix, strVal)
switch v := v.Interface().(type) {
case error:
return textLine("e" + formatString(v.Error()))
case fmt.Stringer:
return textLine("s" + formatString(v.String()))
}
}
}
@@ -152,6 +102,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
}()
var ptr string
switch t.Kind() {
case reflect.Bool:
return textLine(fmt.Sprint(v.Bool()))
@@ -160,7 +111,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return textLine(fmt.Sprint(v.Uint()))
case reflect.Uint8:
if parentKind == reflect.Slice || parentKind == reflect.Array {
if withinSlice {
return textLine(formatHex(v.Uint()))
}
return textLine(fmt.Sprint(v.Uint()))
@@ -171,121 +122,77 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
case reflect.Complex64, reflect.Complex128:
return textLine(fmt.Sprint(v.Complex()))
case reflect.String:
return opts.formatString("", v.String())
return textLine(formatString(v.String()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
return textLine(formatPointer(value.PointerOf(v), true))
return textLine(formatPointer(v))
case reflect.Struct:
var list textList
v := makeAddressable(v) // needed for retrieveUnexportedField
maxLen := v.NumField()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
for i := 0; i < v.NumField(); i++ {
vv := v.Field(i)
if value.IsZero(vv) {
continue // Elide fields with zero values
}
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
}
sf := t.Field(i)
if supportExporters && !isExported(sf.Name) {
vv = retrieveUnexportedField(v, sf, true)
}
s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs)
list = append(list, textRecord{Key: sf.Name, Value: s})
s := opts.WithTypeMode(autoType).FormatValue(vv, false, m)
list = append(list, textRecord{Key: t.Field(i).Name, Value: s})
}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
return textWrap{"{", list, "}"}
case reflect.Slice:
if v.IsNil() {
return textNil
}
// Check whether this is a []byte of text data.
if t.Elem() == reflect.TypeOf(byte(0)) {
b := v.Bytes()
isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) }
if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
out = opts.formatString("", string(b))
return opts.WithTypeMode(emitType).FormatType(t, out)
}
if opts.PrintAddresses {
ptr = formatPointer(v)
}
fallthrough
case reflect.Array:
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
var list textList
for i := 0; i < v.Len(); i++ {
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
vi := v.Index(i)
if vi.CanAddr() { // Check for cyclic elements
p := vi.Addr()
if m.Visit(p) {
var out textNode
out = textLine(formatPointer(p))
out = opts.WithTypeMode(emitType).FormatType(p.Type(), out)
out = textWrap{"*", out, ""}
list = append(list, textRecord{Value: out})
continue
}
}
s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs)
s := opts.WithTypeMode(elideType).FormatValue(vi, true, m)
list = append(list, textRecord{Value: s})
}
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
if t.Kind() == reflect.Slice && opts.PrintAddresses {
header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap())
out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out}
}
return out
return textWrap{ptr + "{", list, "}"}
case reflect.Map:
if v.IsNil() {
return textNil
}
// Check pointer for cycles.
ptrRef, visited := ptrs.Push(v)
if visited {
return makeLeafReference(ptrRef, opts.PrintAddresses)
if m.Visit(v) {
return textLine(formatPointer(v))
}
defer ptrs.Pop()
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
var list textList
for _, k := range value.SortKeys(v.MapKeys()) {
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
}
sk := formatMapKey(k, false, ptrs)
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs)
sk := formatMapKey(k)
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m)
list = append(list, textRecord{Key: sk, Value: sv})
}
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
return out
if opts.PrintAddresses {
ptr = formatPointer(v)
}
return textWrap{ptr + "{", list, "}"}
case reflect.Ptr:
if v.IsNil() {
return textNil
}
// Check pointer for cycles.
ptrRef, visited := ptrs.Push(v)
if visited {
out = makeLeafReference(ptrRef, opts.PrintAddresses)
return &textWrap{Prefix: "&", Value: out}
if m.Visit(v) || opts.ShallowPointers {
return textLine(formatPointer(v))
}
if opts.PrintAddresses {
ptr = formatPointer(v)
}
defer ptrs.Pop()
skipType = true // Let the underlying value print the type instead
out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
out = &textWrap{Prefix: "&", Value: out}
return out
return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), false, m), ""}
case reflect.Interface:
if v.IsNil() {
return textNil
@@ -293,67 +200,19 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
// Interfaces accept different concrete types,
// so configure the underlying value to explicitly print the type.
skipType = true // Print the concrete type instead
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), false, m)
default:
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
}
}
func (opts formatOptions) formatString(prefix, s string) textNode {
maxLen := len(s)
maxLines := strings.Count(s, "\n") + 1
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
}
// For multiline strings, use the triple-quote syntax,
// but only use it when printing removed or inserted nodes since
// we only want the extra verbosity for those cases.
lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+')
for i := 0; i < len(lines) && isTripleQuoted; i++ {
lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
isPrintable := func(r rune) bool {
return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
}
line := lines[i]
isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen
}
if isTripleQuoted {
var list textList
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
for i, line := range lines {
if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 {
comment := commentString(fmt.Sprintf("%d elided lines", numElided))
list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment})
break
}
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true})
}
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
return &textWrap{Prefix: "(", Value: list, Suffix: ")"}
}
// Format the string as a single-line quoted string.
if len(s) > maxLen+len(textEllipsis) {
return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis))
}
return textLine(prefix + formatString(s))
}
// formatMapKey formats v as if it were a map key.
// The result is guaranteed to be a single line.
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
func formatMapKey(v reflect.Value) string {
var opts formatOptions
opts.DiffMode = diffIdentical
opts.TypeMode = elideType
opts.PrintAddresses = disambiguate
opts.AvoidStringer = disambiguate
opts.QualifiedNames = disambiguate
opts.VerbosityLevel = maxVerbosityPreset
opts.LimitVerbosity = true
s := opts.FormatValue(v, reflect.Map, ptrs).String()
opts.ShallowPointers = true
s := opts.FormatValue(v, false, visitedPointers{}).String()
return strings.TrimSpace(s)
}
@@ -371,7 +230,7 @@ func formatString(s string) string {
rawInvalid := func(r rune) bool {
return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
}
if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 {
if strings.IndexFunc(s, rawInvalid) < 0 {
return "`" + s + "`"
}
return qs
@@ -400,3 +259,23 @@ func formatHex(u uint64) string {
}
return fmt.Sprintf(f, u)
}
// formatPointer prints the address of the pointer.
func formatPointer(v reflect.Value) string {
p := v.Pointer()
if flags.Deterministic {
p = 0xdeadf00f // Only used for stable testing purposes
}
return fmt.Sprintf("⟪0x%x⟫", p)
}
type visitedPointers map[value.Pointer]struct{}
// Visit inserts pointer v into the visited map and reports whether it had
// already been visited before.
func (m visitedPointers) Visit(v reflect.Value) bool {
p := value.PointerOf(v)
_, visited := m[p]
m[p] = struct{}{}
return visited
}

View File

@@ -1,15 +1,13 @@
// 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.
// license that can be found in the LICENSE.md file.
package cmp
import (
"bytes"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"unicode"
"unicode/utf8"
@@ -25,35 +23,14 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
return false // Must be formatting in diff mode
case v.NumDiff == 0:
return false // No differences detected
case v.NumIgnored+v.NumCompared+v.NumTransformed > 0:
// TODO: Handle the case where someone uses bytes.Equal on a large slice.
return false // Some custom option was used to determined equality
case !v.ValueX.IsValid() || !v.ValueY.IsValid():
return false // Both values must be valid
case v.NumIgnored > 0:
return false // Some ignore option was used
case v.NumTransformed > 0:
return false // Some transform option was used
case v.NumCompared > 1:
return false // More than one comparison was used
case v.NumCompared == 1 && v.Type.Name() != "":
// The need for cmp to check applicability of options on every element
// in a slice is a significant performance detriment for large []byte.
// The workaround is to specify Comparer(bytes.Equal),
// which enables cmp to compare []byte more efficiently.
// If they differ, we still want to provide batched diffing.
// The logic disallows named types since they tend to have their own
// String method, with nicer formatting than what this provides.
return false
}
// Check whether this is an interface with the same concrete types.
t := v.Type
vx, vy := v.ValueX, v.ValueY
if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() {
vx, vy = vx.Elem(), vy.Elem()
t = vx.Type()
}
// Check whether we provide specialized diffing for this type.
switch t.Kind() {
switch t := v.Type; t.Kind() {
case reflect.String:
case reflect.Array, reflect.Slice:
// Only slices of primitive types have specialized handling.
@@ -65,11 +42,6 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
return false
}
// Both slice values have to be non-empty.
if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) {
return false
}
// If a sufficient number of elements already differ,
// use specialized formatting even if length requirement is not met.
if v.NumDiff > v.NumSame {
@@ -81,7 +53,7 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
// Use specialized string diffing for longer slices or strings.
const minLength = 64
return vx.Len() >= minLength && vy.Len() >= minLength
return v.ValueX.Len() >= minLength && v.ValueY.Len() >= minLength
}
// FormatDiffSlice prints a diff for the slices (or strings) represented by v.
@@ -90,23 +62,17 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
assert(opts.DiffMode == diffUnknown)
t, vx, vy := v.Type, v.ValueX, v.ValueY
if t.Kind() == reflect.Interface {
vx, vy = vx.Elem(), vy.Elem()
t = vx.Type()
opts = opts.WithTypeMode(emitType)
}
// Auto-detect the type of the data.
var isLinedText, isText, isBinary bool
var sx, sy string
var ssx, ssy []string
var isString, isMostlyText, isPureLinedText, isBinary bool
switch {
case t.Kind() == reflect.String:
sx, sy = vx.String(), vy.String()
isString = true
isText = true // Initial estimate, verify later
case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)):
sx, sy = string(vx.Bytes()), string(vy.Bytes())
isString = true
isBinary = true // Initial estimate, verify later
case t.Kind() == reflect.Array:
// Arrays need to be addressable for slice operations to work.
vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem()
@@ -114,12 +80,13 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
vy2.Set(vy)
vx, vy = vx2, vy2
}
if isString {
var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int
if isText || isBinary {
var numLines, lastLineIdx, maxLineLen int
isBinary = false
for i, r := range sx + sy {
numTotalRunes++
if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError {
numValidRunes++
if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
isBinary = true
break
}
if r == '\n' {
if maxLineLen < i-lastLineIdx {
@@ -129,26 +96,8 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
numLines++
}
}
isPureText := numValidRunes == numTotalRunes
isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes))
isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024
isBinary = !isMostlyText
// Avoid diffing by lines if it produces a significantly more complex
// edit script than diffing by bytes.
if isPureLinedText {
ssx = strings.Split(sx, "\n")
ssy = strings.Split(sy, "\n")
esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result {
return diff.BoolResult(ssx[ix] == ssy[iy])
})
esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result {
return diff.BoolResult(sx[ix] == sy[iy])
})
efficiencyLines := float64(esLines.Dist()) / float64(len(esLines))
efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes))
isPureLinedText = efficiencyLines < 4*efficiencyBytes
}
isText = !isBinary
isLinedText = isText && numLines >= 4 && maxLineLen <= 256
}
// Format the string into printable records.
@@ -157,7 +106,9 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
switch {
// If the text appears to be multi-lined text,
// then perform differencing across individual lines.
case isPureLinedText:
case isLinedText:
ssx := strings.Split(sx, "\n")
ssy := strings.Split(sy, "\n")
list = opts.formatDiffSlice(
reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line",
func(v reflect.Value, d diffMode) textRecord {
@@ -166,87 +117,10 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
},
)
delim = "\n"
// If possible, use a custom triple-quote (""") syntax for printing
// differences in a string literal. This format is more readable,
// but has edge-cases where differences are visually indistinguishable.
// This format is avoided under the following conditions:
// • A line starts with `"""`
// • A line starts with "..."
// • A line contains non-printable characters
// • Adjacent different lines differ only by whitespace
//
// For example:
// """
// ... // 3 identical lines
// foo
// bar
// - baz
// + BAZ
// """
isTripleQuoted := true
prevRemoveLines := map[string]bool{}
prevInsertLines := map[string]bool{}
var list2 textList
list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
for _, r := range list {
if !r.Value.Equal(textEllipsis) {
line, _ := strconv.Unquote(string(r.Value.(textLine)))
line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
normLine := strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1 // drop whitespace to avoid visually indistinguishable output
}
return r
}, line)
isPrintable := func(r rune) bool {
return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
}
isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == ""
switch r.Diff {
case diffRemoved:
isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine]
prevRemoveLines[normLine] = true
case diffInserted:
isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine]
prevInsertLines[normLine] = true
}
if !isTripleQuoted {
break
}
r.Value = textLine(line)
r.ElideComma = true
}
if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group
prevRemoveLines = map[string]bool{}
prevInsertLines = map[string]bool{}
}
list2 = append(list2, r)
}
if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 {
list2 = list2[:len(list2)-1] // elide single empty line at the end
}
list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
if isTripleQuoted {
var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"}
switch t.Kind() {
case reflect.String:
if t != reflect.TypeOf(string("")) {
out = opts.FormatType(t, out)
}
case reflect.Slice:
// Always emit type for slices since the triple-quote syntax
// looks like a string (not a slice).
opts = opts.WithTypeMode(emitType)
out = opts.FormatType(t, out)
}
return out
}
// If the text appears to be single-lined text,
// then perform differencing in approximately fixed-sized chunks.
// The output is printed as quoted strings.
case isMostlyText:
case isText:
list = opts.formatDiffSlice(
reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte",
func(v reflect.Value, d diffMode) textRecord {
@@ -254,7 +128,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
return textRecord{Diff: d, Value: textLine(s)}
},
)
delim = ""
// If the text appears to be binary data,
// then perform differencing in approximately fixed-sized chunks.
// The output is inspired by hexdump.
@@ -271,7 +145,6 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
return textRecord{Diff: d, Value: textLine(s), Comment: comment}
},
)
// For all other slices of primitive types,
// then perform differencing in approximately fixed-sized chunks.
// The size of each chunk depends on the width of the element kind.
@@ -314,8 +187,8 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
// Wrap the output with appropriate type information.
var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
if !isMostlyText {
var out textNode = textWrap{"{", list, "}"}
if !isText {
// The "{...}" byte-sequence literal is not valid Go syntax for strings.
// Emit the type for extra clarity (e.g. "string{...}").
if t.Kind() == reflect.String {
@@ -325,12 +198,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
switch t.Kind() {
case reflect.String:
out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf(string("")) {
out = opts.FormatType(t, out)
}
case reflect.Slice:
out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf([]byte(nil)) {
out = opts.FormatType(t, out)
}
@@ -354,11 +227,8 @@ func (opts formatOptions) formatDiffSlice(
vx, vy reflect.Value, chunkSize int, name string,
makeRec func(reflect.Value, diffMode) textRecord,
) (list textList) {
eq := func(ix, iy int) bool {
return vx.Index(ix).Interface() == vy.Index(iy).Interface()
}
es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
return diff.BoolResult(eq(ix, iy))
es := diff.Difference(vx.Len(), vy.Len(), func(ix int, iy int) diff.Result {
return diff.BoolResult(vx.Index(ix).Interface() == vy.Index(iy).Interface())
})
appendChunks := func(v reflect.Value, d diffMode) int {
@@ -374,23 +244,9 @@ func (opts formatOptions) formatDiffSlice(
return n0 - v.Len()
}
var numDiffs int
maxLen := -1
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
opts.VerbosityLevel--
}
groups := coalesceAdjacentEdits(name, es)
groups = coalesceInterveningIdentical(groups, chunkSize/4)
groups = cleanupSurroundingIdentical(groups, eq)
maxGroup := diffStats{Name: name}
for i, ds := range groups {
if maxLen >= 0 && numDiffs >= maxLen {
maxGroup = maxGroup.Append(ds)
continue
}
// Print equal.
if ds.NumDiff() == 0 {
// Compute the number of leading and trailing equal bytes to print.
@@ -419,53 +275,36 @@ func (opts formatOptions) formatDiffSlice(
}
// Print unequal.
len0 := len(list)
nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
vx = vx.Slice(nx, vx.Len())
ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
vy = vy.Slice(ny, vy.Len())
numDiffs += len(list) - len0
}
if maxGroup.IsZero() {
assert(vx.Len() == 0 && vy.Len() == 0)
} else {
list.AppendEllipsis(maxGroup)
}
assert(vx.Len() == 0 && vy.Len() == 0)
return list
}
// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent
// equal or unequal counts.
//
// Example:
//
// Input: "..XXY...Y"
// Output: [
// {NumIdentical: 2},
// {NumRemoved: 2, NumInserted 1},
// {NumIdentical: 3},
// {NumInserted: 1},
// ]
//
func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) {
var prevMode byte
lastStats := func(mode byte) *diffStats {
if prevMode != mode {
var prevCase int // Arbitrary index into which case last occurred
lastStats := func(i int) *diffStats {
if prevCase != i {
groups = append(groups, diffStats{Name: name})
prevMode = mode
prevCase = i
}
return &groups[len(groups)-1]
}
for _, e := range es {
switch e {
case diff.Identity:
lastStats('=').NumIdentical++
lastStats(1).NumIdentical++
case diff.UniqueX:
lastStats('!').NumRemoved++
lastStats(2).NumRemoved++
case diff.UniqueY:
lastStats('!').NumInserted++
lastStats(2).NumInserted++
case diff.Modified:
lastStats('!').NumModified++
lastStats(2).NumModified++
}
}
return groups
@@ -475,35 +314,6 @@ func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats)
// equal groups into adjacent unequal groups that currently result in a
// dual inserted/removed printout. This acts as a high-pass filter to smooth
// out high-frequency changes within the windowSize.
//
// Example:
//
// WindowSize: 16,
// Input: [
// {NumIdentical: 61}, // group 0
// {NumRemoved: 3, NumInserted: 1}, // group 1
// {NumIdentical: 6}, // ├── coalesce
// {NumInserted: 2}, // ├── coalesce
// {NumIdentical: 1}, // ├── coalesce
// {NumRemoved: 9}, // └── coalesce
// {NumIdentical: 64}, // group 2
// {NumRemoved: 3, NumInserted: 1}, // group 3
// {NumIdentical: 6}, // ├── coalesce
// {NumInserted: 2}, // ├── coalesce
// {NumIdentical: 1}, // ├── coalesce
// {NumRemoved: 7}, // ├── coalesce
// {NumIdentical: 1}, // ├── coalesce
// {NumRemoved: 2}, // └── coalesce
// {NumIdentical: 63}, // group 4
// ]
// Output: [
// {NumIdentical: 61},
// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3},
// {NumIdentical: 64},
// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3},
// {NumIdentical: 63},
// ]
//
func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats {
groups, groupsOrig := groups[:0], groups
for i, ds := range groupsOrig {
@@ -523,91 +333,3 @@ func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStat
}
return groups
}
// cleanupSurroundingIdentical scans through all unequal groups, and
// moves any leading sequence of equal elements to the preceding equal group and
// moves and trailing sequence of equal elements to the succeeding equal group.
//
// This is necessary since coalesceInterveningIdentical may coalesce edit groups
// together such that leading/trailing spans of equal elements becomes possible.
// Note that this can occur even with an optimal diffing algorithm.
//
// Example:
//
// Input: [
// {NumIdentical: 61},
// {NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements
// {NumIdentical: 67},
// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, // assume 10 trailing identical elements
// {NumIdentical: 54},
// ]
// Output: [
// {NumIdentical: 64}, // incremented by 3
// {NumRemoved: 9},
// {NumIdentical: 67},
// {NumRemoved: 9},
// {NumIdentical: 64}, // incremented by 10
// ]
//
func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats {
var ix, iy int // indexes into sequence x and y
for i, ds := range groups {
// Handle equal group.
if ds.NumDiff() == 0 {
ix += ds.NumIdentical
iy += ds.NumIdentical
continue
}
// Handle unequal group.
nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified
ny := ds.NumIdentical + ds.NumInserted + ds.NumModified
var numLeadingIdentical, numTrailingIdentical int
for i := 0; i < nx && i < ny && eq(ix+i, iy+i); i++ {
numLeadingIdentical++
}
for i := 0; i < nx && i < ny && eq(ix+nx-1-i, iy+ny-1-i); i++ {
numTrailingIdentical++
}
if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 {
if numLeadingIdentical > 0 {
// Remove leading identical span from this group and
// insert it into the preceding group.
if i-1 >= 0 {
groups[i-1].NumIdentical += numLeadingIdentical
} else {
// No preceding group exists, so prepend a new group,
// but do so after we finish iterating over all groups.
defer func() {
groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...)
}()
}
// Increment indexes since the preceding group would have handled this.
ix += numLeadingIdentical
iy += numLeadingIdentical
}
if numTrailingIdentical > 0 {
// Remove trailing identical span from this group and
// insert it into the succeeding group.
if i+1 < len(groups) {
groups[i+1].NumIdentical += numTrailingIdentical
} else {
// No succeeding group exists, so append a new group,
// but do so after we finish iterating over all groups.
defer func() {
groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical})
}()
}
// Do not increment indexes since the succeeding group will handle this.
}
// Update this group since some identical elements were removed.
nx -= numIdentical
ny -= numIdentical
groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny}
}
ix += nx
iy += ny
}
return groups
}

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
package cmp
@@ -10,15 +10,12 @@ import (
"math/rand"
"strings"
"time"
"unicode/utf8"
"github.com/google/go-cmp/cmp/internal/flags"
)
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
const maxColumnLength = 80
type indentMode int
func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
@@ -94,22 +91,21 @@ type textNode interface {
// textWrap is a wrapper that concatenates a prefix and/or a suffix
// to the underlying node.
type textWrap struct {
Prefix string // e.g., "bytes.Buffer{"
Value textNode // textWrap | textList | textLine
Suffix string // e.g., "}"
Metadata interface{} // arbitrary metadata; has no effect on formatting
Prefix string // e.g., "bytes.Buffer{"
Value textNode // textWrap | textList | textLine
Suffix string // e.g., "}"
}
func (s *textWrap) Len() int {
func (s textWrap) Len() int {
return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
}
func (s1 *textWrap) Equal(s2 textNode) bool {
if s2, ok := s2.(*textWrap); ok {
func (s1 textWrap) Equal(s2 textNode) bool {
if s2, ok := s2.(textWrap); ok {
return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
}
return false
}
func (s *textWrap) String() string {
func (s textWrap) String() string {
var d diffMode
var n indentMode
_, s2 := s.formatCompactTo(nil, d)
@@ -118,7 +114,7 @@ func (s *textWrap) String() string {
b = append(b, '\n') // Trailing newline
return string(b)
}
func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
n0 := len(b) // Original buffer length
b = append(b, s.Prefix...)
b, s.Value = s.Value.formatCompactTo(b, d)
@@ -128,7 +124,7 @@ func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
}
return b, s
}
func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
b = append(b, s.Prefix...)
b = s.Value.formatExpandedTo(b, d, n)
b = append(b, s.Suffix...)
@@ -140,23 +136,22 @@ func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
// of the textList.formatCompactTo method.
type textList []textRecord
type textRecord struct {
Diff diffMode // e.g., 0 or '-' or '+'
Key string // e.g., "MyField"
Value textNode // textWrap | textLine
ElideComma bool // avoid trailing comma
Comment fmt.Stringer // e.g., "6 identical fields"
Diff diffMode // e.g., 0 or '-' or '+'
Key string // e.g., "MyField"
Value textNode // textWrap | textLine
Comment fmt.Stringer // e.g., "6 identical fields"
}
// AppendEllipsis appends a new ellipsis node to the list if none already
// exists at the end. If cs is non-zero it coalesces the statistics with the
// previous diffStats.
func (s *textList) AppendEllipsis(ds diffStats) {
hasStats := !ds.IsZero()
hasStats := ds != diffStats{}
if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
if hasStats {
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
*s = append(*s, textRecord{Value: textEllipsis, Comment: ds})
} else {
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
*s = append(*s, textRecord{Value: textEllipsis})
}
return
}
@@ -196,7 +191,7 @@ func (s1 textList) Equal(s2 textNode) bool {
}
func (s textList) String() string {
return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
return textWrap{"{", s, "}"}.String()
}
func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
@@ -226,7 +221,7 @@ func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
}
// Force multi-lined output when printing a removed/inserted node that
// is sufficiently long.
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 {
multiLine = true
}
if !multiLine {
@@ -241,50 +236,16 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
_, isLine := r.Value.(textLine)
return r.Key == "" || !isLine
},
func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
func(r textRecord) int { return len(r.Key) },
)
alignValueLens := s.alignLens(
func(r textRecord) bool {
_, isLine := r.Value.(textLine)
return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
},
func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },
func(r textRecord) int { return len(r.Value.(textLine)) },
)
// Format lists of simple lists in a batched form.
// If the list is sequence of only textLine values,
// then batch multiple values on a single line.
var isSimple bool
for _, r := range s {
_, isLine := r.Value.(textLine)
isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil
if !isSimple {
break
}
}
if isSimple {
n++
var batch []byte
emitBatch := func() {
if len(batch) > 0 {
b = n.appendIndent(append(b, '\n'), d)
b = append(b, bytes.TrimRight(batch, " ")...)
batch = batch[:0]
}
}
for _, r := range s {
line := r.Value.(textLine)
if len(batch)+len(line)+len(", ") > maxColumnLength {
emitBatch()
}
batch = append(batch, line...)
batch = append(batch, ", "...)
}
emitBatch()
n--
return n.appendIndent(append(b, '\n'), d)
}
// Format the list as a multi-lined output.
n++
for i, r := range s {
@@ -295,7 +256,7 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
b = alignKeyLens[i].appendChar(b, ' ')
b = r.Value.formatExpandedTo(b, d|r.Diff, n)
if !r.ElideComma {
if !r.Value.Equal(textEllipsis) {
b = append(b, ',')
}
b = alignValueLens[i].appendChar(b, ' ')
@@ -371,11 +332,6 @@ type diffStats struct {
NumModified int
}
func (s diffStats) IsZero() bool {
s.Name = ""
return s == diffStats{}
}
func (s diffStats) NumDiff() int {
return s.NumRemoved + s.NumInserted + s.NumModified
}

View File

@@ -1,6 +1,6 @@
// 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.
// license that can be found in the LICENSE.md file.
package cmp

2
vendor/modules.txt vendored
View File

@@ -1,4 +1,4 @@
# github.com/google/go-cmp v0.5.6
# github.com/google/go-cmp v0.4.1
## explicit
github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/internal/diff