Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e3b6508106 | ||
![]() |
8c83122eca | ||
![]() |
f2ac02f446 | ||
![]() |
7704281b49 | ||
![]() |
d77d28b101 | ||
![]() |
858d552240 | ||
![]() |
0191d5c84e | ||
![]() |
aa374638bf | ||
![]() |
d9deca7e18 | ||
![]() |
417d6d51e6 | ||
![]() |
54c186c94e |
25
.github/workflows/go.yml
vendored
Normal file
25
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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.16
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
examples/
|
@@ -1,3 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- "1.12.5"
|
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Artem Piskun
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
91
README.md
91
README.md
@@ -1,24 +1,32 @@
|
|||||||
# REST API 2.0 Discogs.com client
|
# REST API 2.0 Discogs.com client
|
||||||
|
|
||||||
[](https://travis-ci.org/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.
|
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.
|
||||||
|
|
||||||
### Feauteres
|
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
|
||||||
* Database
|
* Database
|
||||||
* [Releases](#releases)
|
* [Releases](#releases)
|
||||||
* Release Rating
|
* Release Rating
|
||||||
* Master Releases
|
* Master Releases
|
||||||
* Master Versions
|
* Master Versions
|
||||||
* Artists
|
* Artists
|
||||||
* Artist Releases
|
* Artist Releases
|
||||||
* Label
|
* Label
|
||||||
* All Label Releases
|
* All Label Releases
|
||||||
* [Search](#search)
|
* [Search](#search)
|
||||||
|
* [User Collection](#user-collection)
|
||||||
|
* Collection Folders
|
||||||
|
* Folder
|
||||||
|
* Collection Items by Folder
|
||||||
|
* Collection Items by Release
|
||||||
|
* [Marketplace](#marketplace)
|
||||||
|
* Price Suggestions
|
||||||
|
* Release Statistics
|
||||||
|
|
||||||
Install
|
Install
|
||||||
--------
|
--------
|
||||||
go get -u github.com/irlndts/go-discogs
|
go get github.com/irlndts/go-discogs
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
---------
|
---------
|
||||||
@@ -28,19 +36,20 @@ First of all import library and init client variable. According to discogs api d
|
|||||||
import "github.com/irlndts/go-discogs"
|
import "github.com/irlndts/go-discogs"
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
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).
|
||||||
This is token way example:
|
|
||||||
```go
|
```go
|
||||||
client, err := discogs.NewClient(&discogs.Options{
|
client, err := discogs.New(&discogs.Options{
|
||||||
UserAgent: "Some Name",
|
UserAgent: "Some Name",
|
||||||
Currency: "EUR", // optional, "USD" (default), "GBP", "EUR", "CAD", "AUD", "JPY", "CHF", "MXN", "BRL", "NZD", "SEK", "ZAR" are allowed
|
Currency: "EUR", // optional, "USD" (default), "GBP", "EUR", "CAD", "AUD", "JPY", "CHF", "MXN", "BRL", "NZD", "SEK", "ZAR" are allowed
|
||||||
Token: "Some Token", // optional
|
Token: "Some Token", // optional
|
||||||
|
URL: "https://api.discogs.com", // optional
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Releases
|
#### Releases
|
||||||
```go
|
```go
|
||||||
release, _ := client.Database.Release(9893847)
|
release, _ := client.Release(9893847)
|
||||||
fmt.Println(release.Artists[0].Name, " - ", release.Title)
|
fmt.Println(release.Artists[0].Name, " - ", release.Title)
|
||||||
// St. Petersburg Ska-Jazz Review - Elephant Riddim
|
// St. Petersburg Ska-Jazz Review - Elephant Riddim
|
||||||
```
|
```
|
||||||
@@ -72,18 +81,60 @@ type SearchRequest struct {
|
|||||||
Contributer string // search contributor usernames (optional)
|
Contributer string // search contributor usernames (optional)
|
||||||
|
|
||||||
Page int // optional
|
Page int // optional
|
||||||
PerPage int // optional
|
PerPage int // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example
|
|
||||||
```go
|
```go
|
||||||
request := discogs.SearchRequest{Artist: "reggaenauts", ReleaseTitle: "river rock", Page: 0, PerPage: 1}
|
request := discogs.SearchRequest{Artist: "reggaenauts", ReleaseTitle: "river rock", Page: 0, PerPage: 1}
|
||||||
search, _ := client.Search.Search(request)
|
search, _ := client.Search(request)
|
||||||
|
|
||||||
for _, r := range search.Results {
|
for _, r := range search.Results {
|
||||||
fmt.Println(r.Title)
|
fmt.Println(r.Title)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
etc.
|
#### 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)
|
55
database.go
55
database.go
@@ -12,20 +12,39 @@ const (
|
|||||||
mastersURI = "/masters/"
|
mastersURI = "/masters/"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DatabaseService ...
|
// DatabaseService is an interface to work with database.
|
||||||
type DatabaseService struct {
|
type DatabaseService interface {
|
||||||
|
// Artist represents a person in the discogs database.
|
||||||
|
Artist(artistID int) (*Artist, error)
|
||||||
|
// ArtistReleases returns a list of releases and masters associated with the artist.
|
||||||
|
ArtistReleases(artistID int, pagination *Pagination) (*ArtistReleases, error)
|
||||||
|
// Label returns a label.
|
||||||
|
Label(labelID int) (*Label, error)
|
||||||
|
// LabelReleases returns a list of Releases associated with the label.
|
||||||
|
LabelReleases(labelID int, pagination *Pagination) (*LabelReleases, error)
|
||||||
|
// Master returns a master release.
|
||||||
|
Master(masterID int) (*Master, error)
|
||||||
|
// MasterVersions retrieves a list of all Releases that are versions of this master.
|
||||||
|
MasterVersions(masterID int, pagination *Pagination) (*MasterVersions, error)
|
||||||
|
// Release returns release by release's ID.
|
||||||
|
Release(releaseID int) (*Release, error)
|
||||||
|
// ReleaseRating retruns community release rating.
|
||||||
|
ReleaseRating(releaseID int) (*ReleaseRating, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type databaseService struct {
|
||||||
url string
|
url string
|
||||||
currency string
|
currency string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDatabaseService(url string, currency string) *DatabaseService {
|
func newDatabaseService(url string, currency string) DatabaseService {
|
||||||
return &DatabaseService{
|
return &databaseService{
|
||||||
url: url,
|
url: url,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release serves relesase response from discogs
|
// Release serves relesase response from discogs.
|
||||||
type Release struct {
|
type Release struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
@@ -63,8 +82,7 @@ type Release struct {
|
|||||||
Year int `json:"year"`
|
Year int `json:"year"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release returns release by release's ID
|
func (s *databaseService) Release(releaseID int) (*Release, error) {
|
||||||
func (s *DatabaseService) Release(releaseID int) (*Release, error) {
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Set("curr_abbr", s.currency)
|
params.Set("curr_abbr", s.currency)
|
||||||
|
|
||||||
@@ -73,14 +91,13 @@ func (s *DatabaseService) Release(releaseID int) (*Release, error) {
|
|||||||
return release, err
|
return release, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseRating serves response for community release rating request
|
// ReleaseRating serves response for community release rating request.
|
||||||
type ReleaseRating struct {
|
type ReleaseRating struct {
|
||||||
ID int `json:"release_id"`
|
ID int `json:"release_id"`
|
||||||
Rating Rating `json:"rating"`
|
Rating Rating `json:"rating"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseRating retruns community release rating
|
func (s *databaseService) ReleaseRating(releaseID int) (*ReleaseRating, error) {
|
||||||
func (s *DatabaseService) ReleaseRating(releaseID int) (*ReleaseRating, error) {
|
|
||||||
var rating *ReleaseRating
|
var rating *ReleaseRating
|
||||||
err := request(s.url+releasesURI+strconv.Itoa(releaseID)+"/rating", nil, &rating)
|
err := request(s.url+releasesURI+strconv.Itoa(releaseID)+"/rating", nil, &rating)
|
||||||
return rating, err
|
return rating, err
|
||||||
@@ -105,8 +122,7 @@ type Artist struct {
|
|||||||
DataQuality string `json:"data_quality"`
|
DataQuality string `json:"data_quality"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artist represents a person in the discogs database
|
func (s *databaseService) Artist(artistID int) (*Artist, error) {
|
||||||
func (s *DatabaseService) Artist(artistID int) (*Artist, error) {
|
|
||||||
var artist *Artist
|
var artist *Artist
|
||||||
err := request(s.url+artistsURI+strconv.Itoa(artistID), nil, &artist)
|
err := request(s.url+artistsURI+strconv.Itoa(artistID), nil, &artist)
|
||||||
return artist, err
|
return artist, err
|
||||||
@@ -118,8 +134,7 @@ type ArtistReleases struct {
|
|||||||
Releases []ReleaseSource `json:"releases"`
|
Releases []ReleaseSource `json:"releases"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArtistReleases returns a list of releases and masters associated with the artist.
|
func (s *databaseService) ArtistReleases(artistID int, pagination *Pagination) (*ArtistReleases, error) {
|
||||||
func (s *DatabaseService) ArtistReleases(artistID int, pagination *Pagination) (*ArtistReleases, error) {
|
|
||||||
var releases *ArtistReleases
|
var releases *ArtistReleases
|
||||||
err := request(s.url+artistsURI+strconv.Itoa(artistID)+"/releases", pagination.params(), &releases)
|
err := request(s.url+artistsURI+strconv.Itoa(artistID)+"/releases", pagination.params(), &releases)
|
||||||
return releases, err
|
return releases, err
|
||||||
@@ -141,8 +156,7 @@ type Label struct {
|
|||||||
DataQuality string `json:"data_quality"`
|
DataQuality string `json:"data_quality"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label returns a label.
|
func (s *databaseService) Label(labelID int) (*Label, error) {
|
||||||
func (s *DatabaseService) Label(labelID int) (*Label, error) {
|
|
||||||
var label *Label
|
var label *Label
|
||||||
err := request(s.url+labelsURI+strconv.Itoa(labelID), nil, &label)
|
err := request(s.url+labelsURI+strconv.Itoa(labelID), nil, &label)
|
||||||
return label, err
|
return label, err
|
||||||
@@ -154,8 +168,7 @@ type LabelReleases struct {
|
|||||||
Releases []ReleaseSource `json:"releases"`
|
Releases []ReleaseSource `json:"releases"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabelReleases returns a list of Releases associated with the label.
|
func (s *databaseService) LabelReleases(labelID int, pagination *Pagination) (*LabelReleases, error) {
|
||||||
func (s *DatabaseService) LabelReleases(labelID int, pagination *Pagination) (*LabelReleases, error) {
|
|
||||||
var releases *LabelReleases
|
var releases *LabelReleases
|
||||||
err := request(s.url+labelsURI+strconv.Itoa(labelID)+"/releases", pagination.params(), &releases)
|
err := request(s.url+labelsURI+strconv.Itoa(labelID)+"/releases", pagination.params(), &releases)
|
||||||
return releases, err
|
return releases, err
|
||||||
@@ -187,8 +200,7 @@ type Master struct {
|
|||||||
DataQuality string `json:"data_quality"`
|
DataQuality string `json:"data_quality"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Master returns a master release
|
func (s *databaseService) Master(masterID int) (*Master, error) {
|
||||||
func (s *DatabaseService) Master(masterID int) (*Master, error) {
|
|
||||||
var master *Master
|
var master *Master
|
||||||
err := request(s.url+mastersURI+strconv.Itoa(masterID), nil, &master)
|
err := request(s.url+mastersURI+strconv.Itoa(masterID), nil, &master)
|
||||||
return master, err
|
return master, err
|
||||||
@@ -200,8 +212,7 @@ type MasterVersions struct {
|
|||||||
Versions []Version `json:"versions"`
|
Versions []Version `json:"versions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MasterVersions retrieves a list of all Releases that are versions of this master
|
func (s *databaseService) MasterVersions(masterID int, pagination *Pagination) (*MasterVersions, error) {
|
||||||
func (s *DatabaseService) MasterVersions(masterID int, pagination *Pagination) (*MasterVersions, error) {
|
|
||||||
var versions *MasterVersions
|
var versions *MasterVersions
|
||||||
err := request(s.url+mastersURI+strconv.Itoa(masterID)+"/versions", pagination.params(), &versions)
|
err := request(s.url+mastersURI+strconv.Itoa(masterID)+"/versions", pagination.params(), &versions)
|
||||||
return versions, err
|
return versions, err
|
||||||
|
@@ -60,7 +60,7 @@ func TestDatabaseServiceRelease(t *testing.T) {
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
||||||
release, err := d.Database.Release(8138518)
|
release, err := d.Release(8138518)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get release: %s", err)
|
t.Fatalf("failed to get release: %s", err)
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ func TestDatabaseServiceMaster(t *testing.T) {
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
||||||
master, err := d.Database.Master(718441)
|
master, err := d.Master(718441)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get master: %s", err)
|
t.Fatalf("failed to get master: %s", err)
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func TestDatabaseServiceArtist(t *testing.T) {
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
d := initDiscogsClient(t, &Options{URL: ts.URL})
|
||||||
artist, err := d.Database.Artist(38661)
|
artist, err := d.Artist(38661)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get master: %s", err)
|
t.Fatalf("failed to get master: %s", err)
|
||||||
}
|
}
|
||||||
|
39
discogs.go
39
discogs.go
@@ -14,22 +14,35 @@ const (
|
|||||||
|
|
||||||
// Options is a set of options to use discogs API client
|
// Options is a set of options to use discogs API client
|
||||||
type Options struct {
|
type Options struct {
|
||||||
URL string
|
// Discogs API endpoint (optional).
|
||||||
Currency string
|
URL string
|
||||||
|
// Currency to use (optional, default is USD).
|
||||||
|
Currency string
|
||||||
|
// UserAgent to to call discogs api with.
|
||||||
UserAgent string
|
UserAgent string
|
||||||
Token string
|
// Token provided by discogs (optional).
|
||||||
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is a Discogs client for making Discogs API requests.
|
// Discogs is an interface for making Discogs API requests.
|
||||||
type Client struct {
|
type Discogs interface {
|
||||||
Database *DatabaseService
|
CollectionService
|
||||||
Search *SearchService
|
DatabaseService
|
||||||
|
MarketPlaceService
|
||||||
|
SearchService
|
||||||
|
}
|
||||||
|
|
||||||
|
type discogs struct {
|
||||||
|
CollectionService
|
||||||
|
DatabaseService
|
||||||
|
SearchService
|
||||||
|
MarketPlaceService
|
||||||
}
|
}
|
||||||
|
|
||||||
var header *http.Header
|
var header *http.Header
|
||||||
|
|
||||||
// NewClient returns a new Client.
|
// New returns a new discogs API client.
|
||||||
func NewClient(o *Options) (*Client, error) {
|
func New(o *Options) (Discogs, error) {
|
||||||
header = &http.Header{}
|
header = &http.Header{}
|
||||||
|
|
||||||
if o == nil || o.UserAgent == "" {
|
if o == nil || o.UserAgent == "" {
|
||||||
@@ -52,9 +65,11 @@ func NewClient(o *Options) (*Client, error) {
|
|||||||
o.URL = discogsAPI
|
o.URL = discogsAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return discogs{
|
||||||
Database: newDatabaseService(o.URL, cur),
|
newCollectionService(o.URL + "/users"),
|
||||||
Search: newSearchService(o.URL + "/database/search"),
|
newDatabaseService(o.URL, cur),
|
||||||
|
newSearchService(o.URL + "/database/search"),
|
||||||
|
newMarketPlaceService(o.URL+"/marketplace", cur),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,11 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testUserAgent = "UnitTestClient/0.0.2 +https://github.com/irlndts/go-discogs"
|
testUserAgent = "UnitTestClient/0.0.2"
|
||||||
|
testUsername = "test_user"
|
||||||
testToken = ""
|
testToken = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
func initDiscogsClient(t *testing.T, options *Options) *Client {
|
func initDiscogsClient(t *testing.T, options *Options) Discogs {
|
||||||
if options == nil {
|
if options == nil {
|
||||||
options = &Options{
|
options = &Options{
|
||||||
UserAgent: testUserAgent,
|
UserAgent: testUserAgent,
|
||||||
@@ -22,7 +23,7 @@ func initDiscogsClient(t *testing.T, options *Options) *Client {
|
|||||||
options.UserAgent = testUserAgent
|
options.UserAgent = testUserAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := NewClient(options)
|
client, err := New(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create client: %s", err)
|
t.Fatalf("failed to create client: %s", err)
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ func initDiscogsClient(t *testing.T, options *Options) *Client {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
options *Options
|
options *Options
|
||||||
err error
|
err error
|
||||||
@@ -53,7 +54,7 @@ func TestNewClient(t *testing.T) {
|
|||||||
for name := range tests {
|
for name := range tests {
|
||||||
tt := tests[name]
|
tt := tests[name]
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if _, err := NewClient(tt.options); err != tt.err {
|
if _, err := New(tt.options); err != tt.err {
|
||||||
t.Errorf("err got=%s; want=%s", err, tt.err)
|
t.Errorf("err got=%s; want=%s", err, tt.err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -66,19 +67,19 @@ func TestCurrency(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
{currency: "", want: "USD", err: nil},
|
{currency: "", want: "USD"},
|
||||||
{currency: "USD", want: "USD", err: nil},
|
{currency: "USD", want: "USD"},
|
||||||
{currency: "GBP", want: "GBP", err: nil},
|
{currency: "GBP", want: "GBP"},
|
||||||
{currency: "EUR", want: "EUR", err: nil},
|
{currency: "EUR", want: "EUR"},
|
||||||
{currency: "CAD", want: "CAD", err: nil},
|
{currency: "CAD", want: "CAD"},
|
||||||
{currency: "AUD", want: "AUD", err: nil},
|
{currency: "AUD", want: "AUD"},
|
||||||
{currency: "JPY", want: "JPY", err: nil},
|
{currency: "JPY", want: "JPY"},
|
||||||
{currency: "CHF", want: "CHF", err: nil},
|
{currency: "CHF", want: "CHF"},
|
||||||
{currency: "MXN", want: "MXN", err: nil},
|
{currency: "MXN", want: "MXN"},
|
||||||
{currency: "BRL", want: "BRL", err: nil},
|
{currency: "BRL", want: "BRL"},
|
||||||
{currency: "NZD", want: "NZD", err: nil},
|
{currency: "NZD", want: "NZD"},
|
||||||
{currency: "SEK", want: "SEK", err: nil},
|
{currency: "SEK", want: "SEK"},
|
||||||
{currency: "ZAR", want: "ZAR", err: nil},
|
{currency: "ZAR", want: "ZAR"},
|
||||||
{currency: "RUR", want: "", err: ErrCurrencyNotSupported},
|
{currency: "RUR", want: "", err: ErrCurrencyNotSupported},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
|
21
doc.go
Normal file
21
doc.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Package discogs is a Go client library for the Discogs API.
|
||||||
|
|
||||||
|
The discogs package provides a client for accessing the Discogs API.
|
||||||
|
First of all import library and init client variable.
|
||||||
|
According to discogs api documentation you must provide your user-agent.
|
||||||
|
|
||||||
|
Some requests require authentification (as any user).
|
||||||
|
According to Discogs, 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. This is token way example:
|
||||||
|
|
||||||
|
client, err := discogs.New(&discogs.Options{
|
||||||
|
UserAgent: "Some Name",
|
||||||
|
Currency: "EUR", // optional, "USD" (default), "GBP", "EUR", "CAD", "AUD", "JPY", "CHF", "MXN", "BRL", "NZD", "SEK", "ZAR" are allowed
|
||||||
|
Token: "Some Token", // optional
|
||||||
|
URL: "https://api.discogs.com", // optional
|
||||||
|
})
|
||||||
|
|
||||||
|
*/
|
||||||
|
package discogs
|
@@ -19,4 +19,7 @@ var (
|
|||||||
ErrUnauthorized = &Error{"authentication required"}
|
ErrUnauthorized = &Error{"authentication required"}
|
||||||
ErrCurrencyNotSupported = &Error{"currency does not supported"}
|
ErrCurrencyNotSupported = &Error{"currency does not supported"}
|
||||||
ErrUserAgentInvalid = &Error{"invalid user-agent"}
|
ErrUserAgentInvalid = &Error{"invalid user-agent"}
|
||||||
|
ErrInvalidReleaseID = &Error{"invalid release id"}
|
||||||
|
ErrInvalidSortKey = &Error{"invalid sort key"}
|
||||||
|
ErrInvalidUsername = &Error{"invalid username"}
|
||||||
)
|
)
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/irlndts/go-discogs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
d, err := discogs.NewClient(&discogs.Options{
|
|
||||||
UserAgent: "TestDiscogsClient/0.0.1 +http://example.com",
|
|
||||||
Currency: "USD",
|
|
||||||
Token: "",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
master, err := d.Database.Master(718441)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("%+v\n", master)
|
|
||||||
}
|
|
4
go.mod
4
go.mod
@@ -1,5 +1,5 @@
|
|||||||
module github.com/irlndts/go-discogs
|
module github.com/irlndts/go-discogs
|
||||||
|
|
||||||
go 1.14
|
go 1.16
|
||||||
|
|
||||||
require github.com/google/go-cmp v0.4.0
|
require github.com/google/go-cmp v0.5.4
|
||||||
|
4
go.sum
4
go.sum
@@ -1,4 +1,4 @@
|
|||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
72
marketplace.go
Normal file
72
marketplace.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
}
|
76
marketplace_test.go
Normal file
76
marketplace_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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)
|
||||||
|
}
|
@@ -78,6 +78,7 @@ type Format struct {
|
|||||||
Descriptions []string `json:"descriptions"`
|
Descriptions []string `json:"descriptions"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Qty string `json:"qty"`
|
Qty string `json:"qty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Company ...
|
// Company ...
|
||||||
@@ -130,8 +131,8 @@ type Page struct {
|
|||||||
|
|
||||||
// URLsList ...
|
// URLsList ...
|
||||||
type URLsList struct {
|
type URLsList struct {
|
||||||
Last string `json:"last"`
|
Last string `json:"last,omitempty"`
|
||||||
Next string `json:"next"`
|
Next string `json:"next,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version ...
|
// Version ...
|
||||||
@@ -188,7 +189,9 @@ type ReleaseSource struct {
|
|||||||
|
|
||||||
// Pagination ...
|
// Pagination ...
|
||||||
type Pagination struct {
|
type Pagination struct {
|
||||||
Sort string // year, title, format
|
// TODO(irlndts): validate requested Sort
|
||||||
|
Sort string // year, title, format etc
|
||||||
|
// TODO(irlndts): validate requested SortOrder
|
||||||
SortOrder string // asc, desc
|
SortOrder string // asc, desc
|
||||||
Page int
|
Page int
|
||||||
PerPage int
|
PerPage int
|
||||||
|
23
search.go
23
search.go
@@ -5,13 +5,22 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SearchService ...
|
// SearchService is an interface to work with search.
|
||||||
type SearchService struct {
|
type SearchService interface {
|
||||||
|
// Search makes search request to discogs.
|
||||||
|
// Issue a search query to database. This endpoint accepts pagination parameters.
|
||||||
|
// Authentication (as any user) is required.
|
||||||
|
// https://www.discogs.com/developers/#page:database,header:database-search
|
||||||
|
Search(req SearchRequest) (*Search, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchService ...
|
||||||
|
type searchService struct {
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSearchService(url string) *SearchService {
|
func newSearchService(url string) SearchService {
|
||||||
return &SearchService{
|
return &searchService{
|
||||||
url: url,
|
url: url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,11 +145,7 @@ type Result struct {
|
|||||||
MasterID int `json:"master_id,omitempty"`
|
MasterID int `json:"master_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search makes search request to discogs.
|
func (s *searchService) Search(req SearchRequest) (*Search, error) {
|
||||||
// Issue a search query to our database. This endpoint accepts pagination parameters.
|
|
||||||
// Authentication (as any user) is required.
|
|
||||||
// https://www.discogs.com/developers/#page:database,header:database-search
|
|
||||||
func (s *SearchService) Search(req SearchRequest) (*Search, error) {
|
|
||||||
var search *Search
|
var search *Search
|
||||||
err := request(s.url, req.params(), &search)
|
err := request(s.url, req.params(), &search)
|
||||||
return search, err
|
return search, err
|
||||||
|
File diff suppressed because one or more lines are too long
135
user_collection.go
Normal file
135
user_collection.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package discogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollectionService is an interface to work with collection.
|
||||||
|
type CollectionService interface {
|
||||||
|
// Retrieve a list of folders in a user’s 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 user’s collection.
|
||||||
|
// If folderID is not 0, authentication with token is required.
|
||||||
|
CollectionItemsByFolder(username string, folderID int, pagination *Pagination) (*CollectionItems, error)
|
||||||
|
// Retrieve the user’s 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 user’s 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 string `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionItems list of items in a user’s 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
|
||||||
|
}
|
174
user_collection_test.go
Normal file
174
user_collection_test.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
85
vendor/github.com/google/go-cmp/cmp/compare.go
generated
vendored
85
vendor/github.com/google/go-cmp/cmp/compare.go
generated
vendored
@@ -1,11 +1,15 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package cmp determines equality of values.
|
// Package cmp determines equality of values.
|
||||||
//
|
//
|
||||||
// This package is intended to be a more powerful and safer alternative to
|
// This package is intended to be a more powerful and safer alternative to
|
||||||
// reflect.DeepEqual for comparing whether two values are semantically equal.
|
// reflect.DeepEqual for comparing whether two values are semantically equal.
|
||||||
|
// It is intended to only be used in tests, as performance is not a goal and
|
||||||
|
// it may panic if it cannot compare the values. Its propensity towards
|
||||||
|
// panicking means that its unsuitable for production environments where a
|
||||||
|
// spurious panic may be fatal.
|
||||||
//
|
//
|
||||||
// The primary features of cmp are:
|
// The primary features of cmp are:
|
||||||
//
|
//
|
||||||
@@ -86,6 +90,52 @@ import (
|
|||||||
// If there is a cycle, then the pointed at values are considered equal
|
// 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.
|
// only if both addresses were previously visited in the same path step.
|
||||||
func Equal(x, y interface{}, opts ...Option) bool {
|
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)
|
vx := reflect.ValueOf(x)
|
||||||
vy := reflect.ValueOf(y)
|
vy := reflect.ValueOf(y)
|
||||||
|
|
||||||
@@ -108,33 +158,7 @@ func Equal(x, y interface{}, opts ...Option) bool {
|
|||||||
t = vx.Type()
|
t = vx.Type()
|
||||||
}
|
}
|
||||||
|
|
||||||
s := newState(opts)
|
return &pathStep{t, vx, vy}
|
||||||
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 {
|
type state struct {
|
||||||
@@ -352,7 +376,7 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
|
|||||||
// assuming that T is assignable to R.
|
// assuming that T is assignable to R.
|
||||||
// Otherwise, it returns the input value as is.
|
// Otherwise, it returns the input value as is.
|
||||||
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
|
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
|
||||||
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143).
|
// TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/22143).
|
||||||
if !flags.AtLeastGo110 {
|
if !flags.AtLeastGo110 {
|
||||||
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
|
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
|
||||||
return reflect.New(t).Elem()
|
return reflect.New(t).Elem()
|
||||||
@@ -362,6 +386,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) compareStruct(t reflect.Type, vx, vy 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 vax, vay reflect.Value // Addressable versions of vx and vy
|
||||||
|
|
||||||
var mayForce, mayForceInit bool
|
var mayForce, mayForceInit bool
|
||||||
@@ -383,6 +408,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
|
|||||||
// For retrieveUnexportedField to work, the parent struct must
|
// For retrieveUnexportedField to work, the parent struct must
|
||||||
// be addressable. Create a new copy of the values if
|
// be addressable. Create a new copy of the values if
|
||||||
// necessary to make them addressable.
|
// necessary to make them addressable.
|
||||||
|
addr = vx.CanAddr() || vy.CanAddr()
|
||||||
vax = makeAddressable(vx)
|
vax = makeAddressable(vx)
|
||||||
vay = makeAddressable(vy)
|
vay = makeAddressable(vy)
|
||||||
}
|
}
|
||||||
@@ -393,6 +419,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
|
|||||||
mayForceInit = true
|
mayForceInit = true
|
||||||
}
|
}
|
||||||
step.mayForce = mayForce
|
step.mayForce = mayForce
|
||||||
|
step.paddr = addr
|
||||||
step.pvx = vax
|
step.pvx = vax
|
||||||
step.pvy = vay
|
step.pvy = vay
|
||||||
step.field = t.Field(i)
|
step.field = t.Field(i)
|
||||||
|
4
vendor/github.com/google/go-cmp/cmp/export_panic.go
generated
vendored
4
vendor/github.com/google/go-cmp/cmp/export_panic.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build purego
|
// +build purego
|
||||||
|
|
||||||
@@ -10,6 +10,6 @@ import "reflect"
|
|||||||
|
|
||||||
const supportExporters = false
|
const supportExporters = false
|
||||||
|
|
||||||
func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value {
|
func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value {
|
||||||
panic("no support for forcibly accessing unexported fields")
|
panic("no support for forcibly accessing unexported fields")
|
||||||
}
|
}
|
||||||
|
22
vendor/github.com/google/go-cmp/cmp/export_unsafe.go
generated
vendored
22
vendor/github.com/google/go-cmp/cmp/export_unsafe.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build !purego
|
// +build !purego
|
||||||
|
|
||||||
@@ -17,9 +17,19 @@ const supportExporters = true
|
|||||||
// a struct such that the value has read-write permissions.
|
// a struct such that the value has read-write permissions.
|
||||||
//
|
//
|
||||||
// The parent struct, v, must be addressable, while f must be a StructField
|
// The parent struct, v, must be addressable, while f must be a StructField
|
||||||
// describing the field to retrieve.
|
// describing the field to retrieve. If addr is false,
|
||||||
func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value {
|
// then the returned value will be shallowed copied to be non-addressable.
|
||||||
// See https://github.com/google/go-cmp/issues/167 for discussion of the
|
func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value {
|
||||||
// following expression.
|
ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
|
||||||
return 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
|
||||||
}
|
}
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build !cmp_debug
|
// +build !cmp_debug
|
||||||
|
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build cmp_debug
|
// +build cmp_debug
|
||||||
|
|
||||||
|
44
vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go
generated
vendored
44
vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package diff implements an algorithm for producing edit-scripts.
|
// Package diff implements an algorithm for producing edit-scripts.
|
||||||
// The edit-script is a sequence of operations needed to transform one list
|
// The edit-script is a sequence of operations needed to transform one list
|
||||||
@@ -12,6 +12,13 @@
|
|||||||
// is more important than obtaining a minimal Levenshtein distance.
|
// is more important than obtaining a minimal Levenshtein distance.
|
||||||
package diff
|
package diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp/internal/flags"
|
||||||
|
)
|
||||||
|
|
||||||
// EditType represents a single operation within an edit-script.
|
// EditType represents a single operation within an edit-script.
|
||||||
type EditType uint8
|
type EditType uint8
|
||||||
|
|
||||||
@@ -112,6 +119,8 @@ func (r Result) Similar() bool {
|
|||||||
return r.NumSame+1 >= r.NumDiff
|
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
|
// Difference reports whether two lists of lengths nx and ny are equal
|
||||||
// given the definition of equality provided as f.
|
// given the definition of equality provided as f.
|
||||||
//
|
//
|
||||||
@@ -177,6 +186,11 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
|||||||
// approximately the square-root of the search budget.
|
// approximately the square-root of the search budget.
|
||||||
searchBudget := 4 * (nx + ny) // O(n)
|
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
|
// The algorithm below is a greedy, meet-in-the-middle algorithm for
|
||||||
// computing sub-optimal edit-scripts between two lists.
|
// computing sub-optimal edit-scripts between two lists.
|
||||||
//
|
//
|
||||||
@@ -194,20 +208,26 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
|||||||
// frontier towards the opposite corner.
|
// frontier towards the opposite corner.
|
||||||
// • This algorithm terminates when either the X coordinates or the
|
// • This algorithm terminates when either the X coordinates or the
|
||||||
// Y coordinates of the forward and reverse frontier points ever intersect.
|
// Y coordinates of the forward and reverse frontier points ever intersect.
|
||||||
//
|
|
||||||
// This algorithm is correct even if searching only in the forward direction
|
// 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
|
// 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
|
// that two lists commonly differ because elements were added to the front
|
||||||
// or end of the other list.
|
// or end of the other list.
|
||||||
//
|
//
|
||||||
// Running the tests with the "cmp_debug" build tag prints a visualization
|
// Non-deterministically start with either the forward or reverse direction
|
||||||
// of the algorithm running in real-time. This is educational for
|
// to introduce some deliberate instability so that we have the flexibility
|
||||||
// understanding how the algorithm works. See debug_enable.go.
|
// to change this algorithm in the future.
|
||||||
f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
|
if flags.Deterministic || randBool {
|
||||||
for {
|
goto forwardSearch
|
||||||
|
} else {
|
||||||
|
goto reverseSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardSearch:
|
||||||
|
{
|
||||||
// Forward search from the beginning.
|
// Forward search from the beginning.
|
||||||
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||||
break
|
goto finishSearch
|
||||||
}
|
}
|
||||||
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||||
// Search in a diagonal pattern for a match.
|
// Search in a diagonal pattern for a match.
|
||||||
@@ -242,10 +262,14 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
|||||||
} else {
|
} else {
|
||||||
fwdFrontier.Y++
|
fwdFrontier.Y++
|
||||||
}
|
}
|
||||||
|
goto reverseSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseSearch:
|
||||||
|
{
|
||||||
// Reverse search from the end.
|
// Reverse search from the end.
|
||||||
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||||
break
|
goto finishSearch
|
||||||
}
|
}
|
||||||
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||||
// Search in a diagonal pattern for a match.
|
// Search in a diagonal pattern for a match.
|
||||||
@@ -280,8 +304,10 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
|||||||
} else {
|
} else {
|
||||||
revFrontier.Y--
|
revFrontier.Y--
|
||||||
}
|
}
|
||||||
|
goto forwardSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finishSearch:
|
||||||
// Join the forward and reverse paths and then append the reverse path.
|
// Join the forward and reverse paths and then append the reverse path.
|
||||||
fwdPath.connect(revPath.point, f)
|
fwdPath.connect(revPath.point, f)
|
||||||
for i := len(revPath.es) - 1; i >= 0; i-- {
|
for i := len(revPath.es) - 1; i >= 0; i-- {
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package flags
|
package flags
|
||||||
|
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/flags/toolchain_legacy.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/flags/toolchain_legacy.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build !go1.10
|
// +build !go1.10
|
||||||
|
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/flags/toolchain_recent.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/flags/toolchain_recent.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build go1.10
|
// +build go1.10
|
||||||
|
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/function/func.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/function/func.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package function provides functionality for identifying function types.
|
// Package function provides functionality for identifying function types.
|
||||||
package function
|
package function
|
||||||
|
157
vendor/github.com/google/go-cmp/cmp/internal/value/name.go
generated
vendored
Normal file
157
vendor/github.com/google/go-cmp/cmp/internal/value/name.go
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// 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
|
||||||
|
}
|
12
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go
generated
vendored
12
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2018, The Go Authors. All rights reserved.
|
// Copyright 2018, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build purego
|
// +build purego
|
||||||
|
|
||||||
@@ -21,3 +21,13 @@ func PointerOf(v reflect.Value) Pointer {
|
|||||||
// assumes that the GC implementation does not use a moving collector.
|
// assumes that the GC implementation does not use a moving collector.
|
||||||
return Pointer{v.Pointer(), v.Type()}
|
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
|
||||||
|
}
|
||||||
|
12
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go
generated
vendored
12
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2018, The Go Authors. All rights reserved.
|
// Copyright 2018, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build !purego
|
// +build !purego
|
||||||
|
|
||||||
@@ -24,3 +24,13 @@ func PointerOf(v reflect.Value) Pointer {
|
|||||||
// which is necessary if the GC ever uses a moving collector.
|
// which is necessary if the GC ever uses a moving collector.
|
||||||
return Pointer{unsafe.Pointer(v.Pointer()), v.Type()}
|
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)
|
||||||
|
}
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/value/sort.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/value/sort.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package value
|
package value
|
||||||
|
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/internal/value/zero.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/internal/value/zero.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package value
|
package value
|
||||||
|
|
||||||
|
7
vendor/github.com/google/go-cmp/cmp/options.go
generated
vendored
7
vendor/github.com/google/go-cmp/cmp/options.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -225,11 +225,14 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
|
|||||||
|
|
||||||
// Unable to Interface implies unexported field without visibility access.
|
// Unable to Interface implies unexported field without visibility access.
|
||||||
if !vx.CanInterface() || !vy.CanInterface() {
|
if !vx.CanInterface() || !vy.CanInterface() {
|
||||||
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"
|
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
|
var name string
|
||||||
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
|
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
|
||||||
// Named type with unexported fields.
|
// Named type with unexported fields.
|
||||||
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
|
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 {
|
} else {
|
||||||
// Unnamed type with unexported fields. Derive PkgPath from field.
|
// Unnamed type with unexported fields. Derive PkgPath from field.
|
||||||
var pkgPath string
|
var pkgPath string
|
||||||
|
9
vendor/github.com/google/go-cmp/cmp/path.go
generated
vendored
9
vendor/github.com/google/go-cmp/cmp/path.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -177,7 +177,8 @@ type structField struct {
|
|||||||
// pvx, pvy, and field are only valid if unexported is true.
|
// pvx, pvy, and field are only valid if unexported is true.
|
||||||
unexported bool
|
unexported bool
|
||||||
mayForce bool // Forcibly allow visibility
|
mayForce bool // Forcibly allow visibility
|
||||||
pvx, pvy reflect.Value // Parent values
|
paddr bool // Was parent addressable?
|
||||||
|
pvx, pvy reflect.Value // Parent values (always addressible)
|
||||||
field reflect.StructField // Field information
|
field reflect.StructField // Field information
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,8 +190,8 @@ func (sf StructField) Values() (vx, vy reflect.Value) {
|
|||||||
|
|
||||||
// Forcibly obtain read-write access to an unexported struct field.
|
// Forcibly obtain read-write access to an unexported struct field.
|
||||||
if sf.mayForce {
|
if sf.mayForce {
|
||||||
vx = retrieveUnexportedField(sf.pvx, sf.field)
|
vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr)
|
||||||
vy = retrieveUnexportedField(sf.pvy, sf.field)
|
vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr)
|
||||||
return vx, vy // CanInterface reports true
|
return vx, vy // CanInterface reports true
|
||||||
}
|
}
|
||||||
return sf.vx, sf.vy // CanInterface reports false
|
return sf.vx, sf.vy // CanInterface reports false
|
||||||
|
7
vendor/github.com/google/go-cmp/cmp/report.go
generated
vendored
7
vendor/github.com/google/go-cmp/cmp/report.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ func (r *defaultReporter) String() string {
|
|||||||
if r.root.NumDiff == 0 {
|
if r.root.NumDiff == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return formatOptions{}.FormatDiff(r.root).String()
|
ptrs := new(pointerReferences)
|
||||||
|
text := formatOptions{}.FormatDiff(r.root, ptrs)
|
||||||
|
resolveReferences(text)
|
||||||
|
return text.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func assert(ok bool) {
|
func assert(ok bool) {
|
||||||
|
202
vendor/github.com/google/go-cmp/cmp/report_compare.go
generated
vendored
202
vendor/github.com/google/go-cmp/cmp/report_compare.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -11,14 +11,6 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp/internal/value"
|
"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.
|
// numContextRecords is the number of surrounding equal records to print.
|
||||||
const numContextRecords = 2
|
const numContextRecords = 2
|
||||||
|
|
||||||
@@ -71,24 +63,66 @@ func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
|
|||||||
opts.TypeMode = t
|
opts.TypeMode = t
|
||||||
return opts
|
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 = 3
|
||||||
|
|
||||||
|
// 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
|
// FormatDiff converts a valueNode tree into a textNode tree, where the later
|
||||||
// is a textual representation of the differences detected in the former.
|
// is a textual representation of the differences detected in the former.
|
||||||
func (opts formatOptions) FormatDiff(v *valueNode) textNode {
|
func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
|
||||||
|
if opts.DiffMode == diffIdentical {
|
||||||
|
opts = opts.WithVerbosity(1)
|
||||||
|
} else {
|
||||||
|
opts = opts.WithVerbosity(3)
|
||||||
|
}
|
||||||
|
|
||||||
// Check whether we have specialized formatting for this node.
|
// Check whether we have specialized formatting for this node.
|
||||||
// This is not necessary, but helpful for producing more readable outputs.
|
// This is not necessary, but helpful for producing more readable outputs.
|
||||||
if opts.CanFormatDiffSlice(v) {
|
if opts.CanFormatDiffSlice(v) {
|
||||||
return opts.FormatDiffSlice(v)
|
return opts.FormatDiffSlice(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parentKind reflect.Kind
|
||||||
|
if v.parent != nil && v.parent.TransformerName == "" {
|
||||||
|
parentKind = v.parent.Type.Kind()
|
||||||
|
}
|
||||||
|
|
||||||
// For leaf nodes, format the value based on the reflect.Values alone.
|
// For leaf nodes, format the value based on the reflect.Values alone.
|
||||||
if v.MaxDepth == 0 {
|
if v.MaxDepth == 0 {
|
||||||
switch opts.DiffMode {
|
switch opts.DiffMode {
|
||||||
case diffUnknown, diffIdentical:
|
case diffUnknown, diffIdentical:
|
||||||
// Format Equal.
|
// Format Equal.
|
||||||
if v.NumDiff == 0 {
|
if v.NumDiff == 0 {
|
||||||
outx := opts.FormatValue(v.ValueX, visitedPointers{})
|
outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
|
||||||
outy := opts.FormatValue(v.ValueY, visitedPointers{})
|
outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
|
||||||
if v.NumIgnored > 0 && v.NumSame == 0 {
|
if v.NumIgnored > 0 && v.NumSame == 0 {
|
||||||
return textEllipsis
|
return textEllipsis
|
||||||
} else if outx.Len() < outy.Len() {
|
} else if outx.Len() < outy.Len() {
|
||||||
@@ -101,8 +135,13 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode {
|
|||||||
// Format unequal.
|
// Format unequal.
|
||||||
assert(opts.DiffMode == diffUnknown)
|
assert(opts.DiffMode == diffUnknown)
|
||||||
var list textList
|
var list textList
|
||||||
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{})
|
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
|
||||||
outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{})
|
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)
|
||||||
|
}
|
||||||
if outx != nil {
|
if outx != nil {
|
||||||
list = append(list, textRecord{Diff: '-', Value: outx})
|
list = append(list, textRecord{Diff: '-', Value: outx})
|
||||||
}
|
}
|
||||||
@@ -111,34 +150,57 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode {
|
|||||||
}
|
}
|
||||||
return opts.WithTypeMode(emitType).FormatType(v.Type, list)
|
return opts.WithTypeMode(emitType).FormatType(v.Type, list)
|
||||||
case diffRemoved:
|
case diffRemoved:
|
||||||
return opts.FormatValue(v.ValueX, visitedPointers{})
|
return opts.FormatValue(v.ValueX, parentKind, ptrs)
|
||||||
case diffInserted:
|
case diffInserted:
|
||||||
return opts.FormatValue(v.ValueY, visitedPointers{})
|
return opts.FormatValue(v.ValueY, parentKind, ptrs)
|
||||||
default:
|
default:
|
||||||
panic("invalid diff mode")
|
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.
|
// Descend into the child value node.
|
||||||
if v.TransformerName != "" {
|
if v.TransformerName != "" {
|
||||||
out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
|
out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
|
||||||
out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
|
out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
|
||||||
return opts.FormatType(v.Type, out)
|
return opts.FormatType(v.Type, out)
|
||||||
} else {
|
} else {
|
||||||
switch k := v.Type.Kind(); k {
|
switch k := v.Type.Kind(); k {
|
||||||
case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map:
|
case reflect.Struct, reflect.Array, reflect.Slice:
|
||||||
return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k))
|
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.Ptr:
|
case reflect.Ptr:
|
||||||
return textWrap{"&", opts.FormatDiff(v.Value), ""}
|
// 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}
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
return opts.WithTypeMode(emitType).FormatDiff(v.Value)
|
out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("%v cannot have children", k))
|
panic(fmt.Sprintf("%v cannot have children", k))
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
|
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
|
||||||
// Derive record name based on the data structure kind.
|
// Derive record name based on the data structure kind.
|
||||||
var name string
|
var name string
|
||||||
var formatKey func(reflect.Value) string
|
var formatKey func(reflect.Value) string
|
||||||
@@ -154,7 +216,17 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
|
|||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
name = "entry"
|
name = "entry"
|
||||||
opts = opts.WithTypeMode(elideType)
|
opts = opts.WithTypeMode(elideType)
|
||||||
formatKey = formatMapKey
|
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--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle unification.
|
// Handle unification.
|
||||||
@@ -163,6 +235,11 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
|
|||||||
var list textList
|
var list textList
|
||||||
var deferredEllipsis bool // Add final "..." to indicate records were dropped
|
var deferredEllipsis bool // Add final "..." to indicate records were dropped
|
||||||
for _, r := range recs {
|
for _, r := range recs {
|
||||||
|
if len(list) == maxLen {
|
||||||
|
deferredEllipsis = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// Elide struct fields that are zero value.
|
// Elide struct fields that are zero value.
|
||||||
if k == reflect.Struct {
|
if k == reflect.Struct {
|
||||||
var isZero bool
|
var isZero bool
|
||||||
@@ -186,23 +263,31 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if out := opts.FormatDiff(r.Value); out != nil {
|
if out := opts.FormatDiff(r.Value, ptrs); out != nil {
|
||||||
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if deferredEllipsis {
|
if deferredEllipsis {
|
||||||
list.AppendEllipsis(diffStats{})
|
list.AppendEllipsis(diffStats{})
|
||||||
}
|
}
|
||||||
return textWrap{"{", list, "}"}
|
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||||||
case diffUnknown:
|
case diffUnknown:
|
||||||
default:
|
default:
|
||||||
panic("invalid diff mode")
|
panic("invalid diff mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle differencing.
|
// Handle differencing.
|
||||||
|
var numDiffs int
|
||||||
var list textList
|
var list textList
|
||||||
|
var keys []reflect.Value // invariant: len(list) == len(keys)
|
||||||
groups := coalesceAdjacentRecords(name, recs)
|
groups := coalesceAdjacentRecords(name, recs)
|
||||||
|
maxGroup := diffStats{Name: name}
|
||||||
for i, ds := range groups {
|
for i, ds := range groups {
|
||||||
|
if maxLen >= 0 && numDiffs >= maxLen {
|
||||||
|
maxGroup = maxGroup.Append(ds)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Handle equal records.
|
// Handle equal records.
|
||||||
if ds.NumDiff() == 0 {
|
if ds.NumDiff() == 0 {
|
||||||
// Compute the number of leading and trailing records to print.
|
// Compute the number of leading and trailing records to print.
|
||||||
@@ -226,16 +311,21 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
|
|||||||
|
|
||||||
// Format the equal values.
|
// Format the equal values.
|
||||||
for _, r := range recs[:numLo] {
|
for _, r := range recs[:numLo] {
|
||||||
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
|
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
|
||||||
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
}
|
}
|
||||||
if numEqual > numLo+numHi {
|
if numEqual > numLo+numHi {
|
||||||
ds.NumIdentical -= numLo + numHi
|
ds.NumIdentical -= numLo + numHi
|
||||||
list.AppendEllipsis(ds)
|
list.AppendEllipsis(ds)
|
||||||
|
for len(keys) < len(list) {
|
||||||
|
keys = append(keys, reflect.Value{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, r := range recs[numEqual-numHi : numEqual] {
|
for _, r := range recs[numEqual-numHi : numEqual] {
|
||||||
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
|
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
|
||||||
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
}
|
}
|
||||||
recs = recs[numEqual:]
|
recs = recs[numEqual:]
|
||||||
continue
|
continue
|
||||||
@@ -247,24 +337,70 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
|
|||||||
case opts.CanFormatDiffSlice(r.Value):
|
case opts.CanFormatDiffSlice(r.Value):
|
||||||
out := opts.FormatDiffSlice(r.Value)
|
out := opts.FormatDiffSlice(r.Value)
|
||||||
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
case r.Value.NumChildren == r.Value.MaxDepth:
|
case r.Value.NumChildren == r.Value.MaxDepth:
|
||||||
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
|
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
|
||||||
outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
|
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)
|
||||||
|
}
|
||||||
if outx != nil {
|
if outx != nil {
|
||||||
list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
|
list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
}
|
}
|
||||||
if outy != nil {
|
if outy != nil {
|
||||||
list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
|
list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
out := opts.FormatDiff(r.Value)
|
out := opts.FormatDiff(r.Value, ptrs)
|
||||||
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
|
||||||
|
keys = append(keys, r.Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recs = recs[ds.NumDiff():]
|
recs = recs[ds.NumDiff():]
|
||||||
|
numDiffs += ds.NumDiff()
|
||||||
}
|
}
|
||||||
assert(len(recs) == 0)
|
if maxGroup.IsZero() {
|
||||||
return textWrap{"{", list, "}"}
|
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: "}"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// coalesceAdjacentRecords coalesces the list of records into groups of
|
// coalesceAdjacentRecords coalesces the list of records into groups of
|
||||||
|
264
vendor/github.com/google/go-cmp/cmp/report_references.go
generated
vendored
Normal file
264
vendor/github.com/google/go-cmp/cmp/report_references.go
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
// 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
|
||||||
|
}
|
298
vendor/github.com/google/go-cmp/cmp/report_reflect.go
generated
vendored
298
vendor/github.com/google/go-cmp/cmp/report_reflect.go
generated
vendored
@@ -1,17 +1,18 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp/internal/flags"
|
|
||||||
"github.com/google/go-cmp/cmp/internal/value"
|
"github.com/google/go-cmp/cmp/internal/value"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,14 +21,22 @@ type formatValueOptions struct {
|
|||||||
// methods like error.Error or fmt.Stringer.String.
|
// methods like error.Error or fmt.Stringer.String.
|
||||||
AvoidStringer bool
|
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,
|
// PrintAddresses controls whether to print the address of all pointers,
|
||||||
// slice elements, and maps.
|
// slice elements, and maps.
|
||||||
PrintAddresses bool
|
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.
|
// FormatType prints the type as if it were wrapping s.
|
||||||
@@ -44,12 +53,15 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
|
|||||||
default:
|
default:
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
if opts.DiffMode == diffIdentical {
|
||||||
|
return s // elide type for identical nodes
|
||||||
|
}
|
||||||
case elideType:
|
case elideType:
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the type label, applying special handling for unnamed types.
|
// Determine the type label, applying special handling for unnamed types.
|
||||||
typeName := t.String()
|
typeName := value.TypeString(t, opts.QualifiedNames)
|
||||||
if t.Name() == "" {
|
if t.Name() == "" {
|
||||||
// According to Go grammar, certain type literals contain symbols that
|
// According to Go grammar, certain type literals contain symbols that
|
||||||
// do not strongly bind to the next lexicographical token (e.g., *T).
|
// do not strongly bind to the next lexicographical token (e.g., *T).
|
||||||
@@ -57,39 +69,77 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
|
|||||||
case reflect.Chan, reflect.Func, reflect.Ptr:
|
case reflect.Chan, reflect.Func, reflect.Ptr:
|
||||||
typeName = "(" + typeName + ")"
|
typeName = "(" + typeName + ")"
|
||||||
}
|
}
|
||||||
typeName = strings.Replace(typeName, "struct {", "struct{", -1)
|
|
||||||
typeName = strings.Replace(typeName, "interface {", "interface{", -1)
|
|
||||||
}
|
}
|
||||||
|
return &textWrap{Prefix: typeName, Value: wrapParens(s)}
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid wrap the value in parenthesis if unnecessary.
|
// wrapParens wraps s with a set of parenthesis, but avoids it if the
|
||||||
if s, ok := s.(textWrap); ok {
|
// wrapped node itself is already surrounded by a pair of parenthesis or braces.
|
||||||
hasParens := strings.HasPrefix(s.Prefix, "(") && strings.HasSuffix(s.Suffix, ")")
|
// It handles unwrapping one level of pointer-reference nodes.
|
||||||
hasBraces := strings.HasPrefix(s.Prefix, "{") && strings.HasSuffix(s.Suffix, "}")
|
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, "}")
|
||||||
if hasParens || hasBraces {
|
if hasParens || hasBraces {
|
||||||
return textWrap{typeName, s, ""}
|
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: ")"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatValue prints the reflect.Value, taking extra care to avoid descending
|
// FormatValue prints the reflect.Value, taking extra care to avoid descending
|
||||||
// into pointers already in m. As pointers are visited, m is also updated.
|
// into pointers already in ptrs. As pointers are visited, ptrs is also updated.
|
||||||
func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out textNode) {
|
func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) {
|
||||||
if !v.IsValid() {
|
if !v.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t := v.Type()
|
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.
|
// Check whether there is an Error or String method to call.
|
||||||
if !opts.AvoidStringer && v.CanInterface() {
|
if !opts.AvoidStringer && v.CanInterface() {
|
||||||
// Avoid calling Error or String methods on nil receivers since many
|
// Avoid calling Error or String methods on nil receivers since many
|
||||||
// implementations crash when doing so.
|
// implementations crash when doing so.
|
||||||
if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
|
if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
|
||||||
switch v := v.Interface().(type) {
|
var prefix, strVal string
|
||||||
case error:
|
func() {
|
||||||
return textLine("e" + formatString(v.Error()))
|
// Swallow and ignore any panics from String or Error.
|
||||||
case fmt.Stringer:
|
defer func() { recover() }()
|
||||||
return textLine("s" + formatString(v.String()))
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,94 +152,140 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var ptr string
|
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return textLine(fmt.Sprint(v.Bool()))
|
return textLine(fmt.Sprint(v.Bool()))
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
return textLine(fmt.Sprint(v.Int()))
|
return textLine(fmt.Sprint(v.Int()))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
// Unnamed uints are usually bytes or words, so use hexadecimal.
|
return textLine(fmt.Sprint(v.Uint()))
|
||||||
if t.PkgPath() == "" || t.Kind() == reflect.Uintptr {
|
case reflect.Uint8:
|
||||||
|
if parentKind == reflect.Slice || parentKind == reflect.Array {
|
||||||
return textLine(formatHex(v.Uint()))
|
return textLine(formatHex(v.Uint()))
|
||||||
}
|
}
|
||||||
return textLine(fmt.Sprint(v.Uint()))
|
return textLine(fmt.Sprint(v.Uint()))
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return textLine(formatHex(v.Uint()))
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
return textLine(fmt.Sprint(v.Float()))
|
return textLine(fmt.Sprint(v.Float()))
|
||||||
case reflect.Complex64, reflect.Complex128:
|
case reflect.Complex64, reflect.Complex128:
|
||||||
return textLine(fmt.Sprint(v.Complex()))
|
return textLine(fmt.Sprint(v.Complex()))
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return textLine(formatString(v.String()))
|
return opts.formatString("", v.String())
|
||||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
return textLine(formatPointer(v))
|
return textLine(formatPointer(value.PointerOf(v), true))
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
var list textList
|
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++ {
|
for i := 0; i < v.NumField(); i++ {
|
||||||
vv := v.Field(i)
|
vv := v.Field(i)
|
||||||
if value.IsZero(vv) {
|
if value.IsZero(vv) {
|
||||||
continue // Elide fields with zero values
|
continue // Elide fields with zero values
|
||||||
}
|
}
|
||||||
s := opts.WithTypeMode(autoType).FormatValue(vv, m)
|
if len(list) == maxLen {
|
||||||
list = append(list, textRecord{Key: t.Field(i).Name, Value: s})
|
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})
|
||||||
}
|
}
|
||||||
return textWrap{"{", list, "}"}
|
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
return textNil
|
return textNil
|
||||||
}
|
}
|
||||||
if opts.PrintAddresses {
|
|
||||||
ptr = formatPointer(v)
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fallthrough
|
fallthrough
|
||||||
case reflect.Array:
|
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
|
var list textList
|
||||||
for i := 0; i < v.Len(); i++ {
|
for i := 0; i < v.Len(); i++ {
|
||||||
vi := v.Index(i)
|
if len(list) == maxLen {
|
||||||
if vi.CanAddr() { // Check for cyclic elements
|
list.AppendEllipsis(diffStats{})
|
||||||
p := vi.Addr()
|
break
|
||||||
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(vi, m)
|
s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs)
|
||||||
list = append(list, textRecord{Value: s})
|
list = append(list, textRecord{Value: s})
|
||||||
}
|
}
|
||||||
return textWrap{ptr + "{", list, "}"}
|
|
||||||
|
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
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
return textNil
|
return textNil
|
||||||
}
|
}
|
||||||
if m.Visit(v) {
|
|
||||||
return textLine(formatPointer(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check pointer for cycles.
|
||||||
|
ptrRef, visited := ptrs.Push(v)
|
||||||
|
if visited {
|
||||||
|
return makeLeafReference(ptrRef, opts.PrintAddresses)
|
||||||
|
}
|
||||||
|
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
|
var list textList
|
||||||
for _, k := range value.SortKeys(v.MapKeys()) {
|
for _, k := range value.SortKeys(v.MapKeys()) {
|
||||||
sk := formatMapKey(k)
|
if len(list) == maxLen {
|
||||||
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), m)
|
list.AppendEllipsis(diffStats{})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sk := formatMapKey(k, false, ptrs)
|
||||||
|
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs)
|
||||||
list = append(list, textRecord{Key: sk, Value: sv})
|
list = append(list, textRecord{Key: sk, Value: sv})
|
||||||
}
|
}
|
||||||
if opts.PrintAddresses {
|
|
||||||
ptr = formatPointer(v)
|
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||||||
}
|
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
|
||||||
return textWrap{ptr + "{", list, "}"}
|
return out
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
return textNil
|
return textNil
|
||||||
}
|
}
|
||||||
if m.Visit(v) || opts.ShallowPointers {
|
|
||||||
return textLine(formatPointer(v))
|
// Check pointer for cycles.
|
||||||
}
|
ptrRef, visited := ptrs.Push(v)
|
||||||
if opts.PrintAddresses {
|
if visited {
|
||||||
ptr = formatPointer(v)
|
out = makeLeafReference(ptrRef, opts.PrintAddresses)
|
||||||
|
return &textWrap{Prefix: "&", Value: out}
|
||||||
}
|
}
|
||||||
|
defer ptrs.Pop()
|
||||||
|
|
||||||
skipType = true // Let the underlying value print the type instead
|
skipType = true // Let the underlying value print the type instead
|
||||||
return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), m), ""}
|
out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
|
||||||
|
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
|
||||||
|
out = &textWrap{Prefix: "&", Value: out}
|
||||||
|
return out
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
return textNil
|
return textNil
|
||||||
@@ -197,19 +293,67 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t
|
|||||||
// Interfaces accept different concrete types,
|
// Interfaces accept different concrete types,
|
||||||
// so configure the underlying value to explicitly print the type.
|
// so configure the underlying value to explicitly print the type.
|
||||||
skipType = true // Print the concrete type instead
|
skipType = true // Print the concrete type instead
|
||||||
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), m)
|
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
|
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.
|
// formatMapKey formats v as if it were a map key.
|
||||||
// The result is guaranteed to be a single line.
|
// The result is guaranteed to be a single line.
|
||||||
func formatMapKey(v reflect.Value) string {
|
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
|
||||||
var opts formatOptions
|
var opts formatOptions
|
||||||
|
opts.DiffMode = diffIdentical
|
||||||
opts.TypeMode = elideType
|
opts.TypeMode = elideType
|
||||||
opts.ShallowPointers = true
|
opts.PrintAddresses = disambiguate
|
||||||
s := opts.FormatValue(v, visitedPointers{}).String()
|
opts.AvoidStringer = disambiguate
|
||||||
|
opts.QualifiedNames = disambiguate
|
||||||
|
opts.VerbosityLevel = maxVerbosityPreset
|
||||||
|
opts.LimitVerbosity = true
|
||||||
|
s := opts.FormatValue(v, reflect.Map, ptrs).String()
|
||||||
return strings.TrimSpace(s)
|
return strings.TrimSpace(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +371,7 @@ func formatString(s string) string {
|
|||||||
rawInvalid := func(r rune) bool {
|
rawInvalid := func(r rune) bool {
|
||||||
return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
|
return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
|
||||||
}
|
}
|
||||||
if strings.IndexFunc(s, rawInvalid) < 0 {
|
if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 {
|
||||||
return "`" + s + "`"
|
return "`" + s + "`"
|
||||||
}
|
}
|
||||||
return qs
|
return qs
|
||||||
@@ -256,23 +400,3 @@ func formatHex(u uint64) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf(f, u)
|
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
|
|
||||||
}
|
|
||||||
|
137
vendor/github.com/google/go-cmp/cmp/report_slices.go
generated
vendored
137
vendor/github.com/google/go-cmp/cmp/report_slices.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -23,11 +24,25 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
|
|||||||
return false // Must be formatting in diff mode
|
return false // Must be formatting in diff mode
|
||||||
case v.NumDiff == 0:
|
case v.NumDiff == 0:
|
||||||
return false // No differences detected
|
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():
|
case !v.ValueX.IsValid() || !v.ValueY.IsValid():
|
||||||
return false // Both values must be valid
|
return false // Both values must be valid
|
||||||
|
case v.Type.Kind() == reflect.Slice && (v.ValueX.Len() == 0 || v.ValueY.Len() == 0):
|
||||||
|
return false // Both slice values have to be non-empty
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t := v.Type; t.Kind() {
|
switch t := v.Type; t.Kind() {
|
||||||
@@ -82,7 +97,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
}
|
}
|
||||||
if isText || isBinary {
|
if isText || isBinary {
|
||||||
var numLines, lastLineIdx, maxLineLen int
|
var numLines, lastLineIdx, maxLineLen int
|
||||||
isBinary = false
|
isBinary = !utf8.ValidString(sx) || !utf8.ValidString(sy)
|
||||||
for i, r := range sx + sy {
|
for i, r := range sx + sy {
|
||||||
if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
|
if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
|
||||||
isBinary = true
|
isBinary = true
|
||||||
@@ -97,7 +112,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isText = !isBinary
|
isText = !isBinary
|
||||||
isLinedText = isText && numLines >= 4 && maxLineLen <= 256
|
isLinedText = isText && numLines >= 4 && maxLineLen <= 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the string into printable records.
|
// Format the string into printable records.
|
||||||
@@ -117,6 +132,83 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
delim = "\n"
|
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,
|
// If the text appears to be single-lined text,
|
||||||
// then perform differencing in approximately fixed-sized chunks.
|
// then perform differencing in approximately fixed-sized chunks.
|
||||||
// The output is printed as quoted strings.
|
// The output is printed as quoted strings.
|
||||||
@@ -129,6 +221,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
delim = ""
|
delim = ""
|
||||||
|
|
||||||
// If the text appears to be binary data,
|
// If the text appears to be binary data,
|
||||||
// then perform differencing in approximately fixed-sized chunks.
|
// then perform differencing in approximately fixed-sized chunks.
|
||||||
// The output is inspired by hexdump.
|
// The output is inspired by hexdump.
|
||||||
@@ -145,6 +238,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
return textRecord{Diff: d, Value: textLine(s), Comment: comment}
|
return textRecord{Diff: d, Value: textLine(s), Comment: comment}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// For all other slices of primitive types,
|
// For all other slices of primitive types,
|
||||||
// then perform differencing in approximately fixed-sized chunks.
|
// then perform differencing in approximately fixed-sized chunks.
|
||||||
// The size of each chunk depends on the width of the element kind.
|
// The size of each chunk depends on the width of the element kind.
|
||||||
@@ -172,7 +266,9 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
switch t.Elem().Kind() {
|
switch t.Elem().Kind() {
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
ss = append(ss, fmt.Sprint(v.Index(i).Int()))
|
ss = append(ss, fmt.Sprint(v.Index(i).Int()))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
ss = append(ss, fmt.Sprint(v.Index(i).Uint()))
|
||||||
|
case reflect.Uint8, reflect.Uintptr:
|
||||||
ss = append(ss, formatHex(v.Index(i).Uint()))
|
ss = append(ss, formatHex(v.Index(i).Uint()))
|
||||||
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
|
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
|
||||||
ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
|
ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
|
||||||
@@ -185,7 +281,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the output with appropriate type information.
|
// Wrap the output with appropriate type information.
|
||||||
var out textNode = textWrap{"{", list, "}"}
|
var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||||||
if !isText {
|
if !isText {
|
||||||
// The "{...}" byte-sequence literal is not valid Go syntax for strings.
|
// The "{...}" byte-sequence literal is not valid Go syntax for strings.
|
||||||
// Emit the type for extra clarity (e.g. "string{...}").
|
// Emit the type for extra clarity (e.g. "string{...}").
|
||||||
@@ -196,12 +292,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|||||||
}
|
}
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)}
|
out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
|
||||||
if t != reflect.TypeOf(string("")) {
|
if t != reflect.TypeOf(string("")) {
|
||||||
out = opts.FormatType(t, out)
|
out = opts.FormatType(t, out)
|
||||||
}
|
}
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)}
|
out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
|
||||||
if t != reflect.TypeOf([]byte(nil)) {
|
if t != reflect.TypeOf([]byte(nil)) {
|
||||||
out = opts.FormatType(t, out)
|
out = opts.FormatType(t, out)
|
||||||
}
|
}
|
||||||
@@ -242,9 +338,22 @@ func (opts formatOptions) formatDiffSlice(
|
|||||||
return n0 - v.Len()
|
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 := coalesceAdjacentEdits(name, es)
|
||||||
groups = coalesceInterveningIdentical(groups, chunkSize/4)
|
groups = coalesceInterveningIdentical(groups, chunkSize/4)
|
||||||
|
maxGroup := diffStats{Name: name}
|
||||||
for i, ds := range groups {
|
for i, ds := range groups {
|
||||||
|
if maxLen >= 0 && numDiffs >= maxLen {
|
||||||
|
maxGroup = maxGroup.Append(ds)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Print equal.
|
// Print equal.
|
||||||
if ds.NumDiff() == 0 {
|
if ds.NumDiff() == 0 {
|
||||||
// Compute the number of leading and trailing equal bytes to print.
|
// Compute the number of leading and trailing equal bytes to print.
|
||||||
@@ -273,12 +382,18 @@ func (opts formatOptions) formatDiffSlice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Print unequal.
|
// Print unequal.
|
||||||
|
len0 := len(list)
|
||||||
nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
|
nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
|
||||||
vx = vx.Slice(nx, vx.Len())
|
vx = vx.Slice(nx, vx.Len())
|
||||||
ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
|
ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
|
||||||
vy = vy.Slice(ny, vy.Len())
|
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
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
88
vendor/github.com/google/go-cmp/cmp/report_text.go
generated
vendored
88
vendor/github.com/google/go-cmp/cmp/report_text.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
@@ -10,12 +10,15 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp/internal/flags"
|
"github.com/google/go-cmp/cmp/internal/flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
|
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
|
||||||
|
|
||||||
|
const maxColumnLength = 80
|
||||||
|
|
||||||
type indentMode int
|
type indentMode int
|
||||||
|
|
||||||
func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
|
func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
|
||||||
@@ -91,21 +94,22 @@ type textNode interface {
|
|||||||
// textWrap is a wrapper that concatenates a prefix and/or a suffix
|
// textWrap is a wrapper that concatenates a prefix and/or a suffix
|
||||||
// to the underlying node.
|
// to the underlying node.
|
||||||
type textWrap struct {
|
type textWrap struct {
|
||||||
Prefix string // e.g., "bytes.Buffer{"
|
Prefix string // e.g., "bytes.Buffer{"
|
||||||
Value textNode // textWrap | textList | textLine
|
Value textNode // textWrap | textList | textLine
|
||||||
Suffix string // e.g., "}"
|
Suffix string // e.g., "}"
|
||||||
|
Metadata interface{} // arbitrary metadata; has no effect on formatting
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s textWrap) Len() int {
|
func (s *textWrap) Len() int {
|
||||||
return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
|
return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
|
||||||
}
|
}
|
||||||
func (s1 textWrap) Equal(s2 textNode) bool {
|
func (s1 *textWrap) Equal(s2 textNode) bool {
|
||||||
if s2, ok := s2.(textWrap); ok {
|
if s2, ok := s2.(*textWrap); ok {
|
||||||
return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
|
return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func (s textWrap) String() string {
|
func (s *textWrap) String() string {
|
||||||
var d diffMode
|
var d diffMode
|
||||||
var n indentMode
|
var n indentMode
|
||||||
_, s2 := s.formatCompactTo(nil, d)
|
_, s2 := s.formatCompactTo(nil, d)
|
||||||
@@ -114,7 +118,7 @@ func (s textWrap) String() string {
|
|||||||
b = append(b, '\n') // Trailing newline
|
b = append(b, '\n') // Trailing newline
|
||||||
return string(b)
|
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
|
n0 := len(b) // Original buffer length
|
||||||
b = append(b, s.Prefix...)
|
b = append(b, s.Prefix...)
|
||||||
b, s.Value = s.Value.formatCompactTo(b, d)
|
b, s.Value = s.Value.formatCompactTo(b, d)
|
||||||
@@ -124,7 +128,7 @@ func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
|||||||
}
|
}
|
||||||
return b, s
|
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 = append(b, s.Prefix...)
|
||||||
b = s.Value.formatExpandedTo(b, d, n)
|
b = s.Value.formatExpandedTo(b, d, n)
|
||||||
b = append(b, s.Suffix...)
|
b = append(b, s.Suffix...)
|
||||||
@@ -136,22 +140,23 @@ func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
|
|||||||
// of the textList.formatCompactTo method.
|
// of the textList.formatCompactTo method.
|
||||||
type textList []textRecord
|
type textList []textRecord
|
||||||
type textRecord struct {
|
type textRecord struct {
|
||||||
Diff diffMode // e.g., 0 or '-' or '+'
|
Diff diffMode // e.g., 0 or '-' or '+'
|
||||||
Key string // e.g., "MyField"
|
Key string // e.g., "MyField"
|
||||||
Value textNode // textWrap | textLine
|
Value textNode // textWrap | textLine
|
||||||
Comment fmt.Stringer // e.g., "6 identical fields"
|
ElideComma bool // avoid trailing comma
|
||||||
|
Comment fmt.Stringer // e.g., "6 identical fields"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendEllipsis appends a new ellipsis node to the list if none already
|
// 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
|
// exists at the end. If cs is non-zero it coalesces the statistics with the
|
||||||
// previous diffStats.
|
// previous diffStats.
|
||||||
func (s *textList) AppendEllipsis(ds diffStats) {
|
func (s *textList) AppendEllipsis(ds diffStats) {
|
||||||
hasStats := ds != diffStats{}
|
hasStats := !ds.IsZero()
|
||||||
if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
|
if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
|
||||||
if hasStats {
|
if hasStats {
|
||||||
*s = append(*s, textRecord{Value: textEllipsis, Comment: ds})
|
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
|
||||||
} else {
|
} else {
|
||||||
*s = append(*s, textRecord{Value: textEllipsis})
|
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -191,7 +196,7 @@ func (s1 textList) Equal(s2 textNode) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s textList) String() string {
|
func (s textList) String() string {
|
||||||
return textWrap{"{", s, "}"}.String()
|
return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
||||||
@@ -221,7 +226,7 @@ func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
|||||||
}
|
}
|
||||||
// Force multi-lined output when printing a removed/inserted node that
|
// Force multi-lined output when printing a removed/inserted node that
|
||||||
// is sufficiently long.
|
// is sufficiently long.
|
||||||
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 {
|
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
|
||||||
multiLine = true
|
multiLine = true
|
||||||
}
|
}
|
||||||
if !multiLine {
|
if !multiLine {
|
||||||
@@ -236,16 +241,50 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
|
|||||||
_, isLine := r.Value.(textLine)
|
_, isLine := r.Value.(textLine)
|
||||||
return r.Key == "" || !isLine
|
return r.Key == "" || !isLine
|
||||||
},
|
},
|
||||||
func(r textRecord) int { return len(r.Key) },
|
func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
|
||||||
)
|
)
|
||||||
alignValueLens := s.alignLens(
|
alignValueLens := s.alignLens(
|
||||||
func(r textRecord) bool {
|
func(r textRecord) bool {
|
||||||
_, isLine := r.Value.(textLine)
|
_, isLine := r.Value.(textLine)
|
||||||
return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
|
return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
|
||||||
},
|
},
|
||||||
func(r textRecord) int { return len(r.Value.(textLine)) },
|
func(r textRecord) int { return utf8.RuneCount(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.
|
// Format the list as a multi-lined output.
|
||||||
n++
|
n++
|
||||||
for i, r := range s {
|
for i, r := range s {
|
||||||
@@ -256,7 +295,7 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
|
|||||||
b = alignKeyLens[i].appendChar(b, ' ')
|
b = alignKeyLens[i].appendChar(b, ' ')
|
||||||
|
|
||||||
b = r.Value.formatExpandedTo(b, d|r.Diff, n)
|
b = r.Value.formatExpandedTo(b, d|r.Diff, n)
|
||||||
if !r.Value.Equal(textEllipsis) {
|
if !r.ElideComma {
|
||||||
b = append(b, ',')
|
b = append(b, ',')
|
||||||
}
|
}
|
||||||
b = alignValueLens[i].appendChar(b, ' ')
|
b = alignValueLens[i].appendChar(b, ' ')
|
||||||
@@ -332,6 +371,11 @@ type diffStats struct {
|
|||||||
NumModified int
|
NumModified int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s diffStats) IsZero() bool {
|
||||||
|
s.Name = ""
|
||||||
|
return s == diffStats{}
|
||||||
|
}
|
||||||
|
|
||||||
func (s diffStats) NumDiff() int {
|
func (s diffStats) NumDiff() int {
|
||||||
return s.NumRemoved + s.NumInserted + s.NumModified
|
return s.NumRemoved + s.NumInserted + s.NumModified
|
||||||
}
|
}
|
||||||
|
2
vendor/github.com/google/go-cmp/cmp/report_value.go
generated
vendored
2
vendor/github.com/google/go-cmp/cmp/report_value.go
generated
vendored
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019, The Go Authors. All rights reserved.
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE.md file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package cmp
|
package cmp
|
||||||
|
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -1,4 +1,4 @@
|
|||||||
# github.com/google/go-cmp v0.4.0
|
# github.com/google/go-cmp v0.5.4
|
||||||
## explicit
|
## explicit
|
||||||
github.com/google/go-cmp/cmp
|
github.com/google/go-cmp/cmp
|
||||||
github.com/google/go-cmp/cmp/internal/diff
|
github.com/google/go-cmp/cmp/internal/diff
|
||||||
|
Reference in New Issue
Block a user