From 681e3a0d5ad539617a368bea529df94929a3baec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20S=C3=B6derlund?= Date: Fri, 31 Mar 2023 11:42:19 +0200 Subject: [PATCH] chore: add unit tests for spec parsing --- catalog/client_entities_getbyid.go | 4 +--- catalog/client_entities_getbyname.go | 4 +--- catalog/client_entities_list.go | 20 +++++------------- catalog/entity.go | 29 +++++++++++++++++++++----- catalog/spec_api_test.go | 26 +++++++++++++++++++++++ catalog/spec_component_test.go | 24 +++++++++++++++++++++ catalog/spec_domain_test.go | 21 +++++++++++++++++++ catalog/spec_group_test.go | 29 ++++++++++++++++++++++++++ catalog/spec_location_test.go | 25 ++++++++++++++++++++++ catalog/spec_resource_test.go | 23 +++++++++++++++++++++ catalog/spec_system.go | 2 +- catalog/spec_system_test.go | 22 ++++++++++++++++++++ catalog/spec_template_test.go | 31 ++++++++++++++++++++++++++++ catalog/spec_user_test.go | 26 +++++++++++++++++++++++ cmd/backstage/go.sum | 2 ++ go.mod | 4 ++++ go.sum | 28 +++++++++++++++++++++++++ 17 files changed, 293 insertions(+), 27 deletions(-) create mode 100644 catalog/spec_api_test.go create mode 100644 catalog/spec_component_test.go create mode 100644 catalog/spec_domain_test.go create mode 100644 catalog/spec_group_test.go create mode 100644 catalog/spec_location_test.go create mode 100644 catalog/spec_resource_test.go create mode 100644 catalog/spec_system_test.go create mode 100644 catalog/spec_template_test.go create mode 100644 catalog/spec_user_test.go create mode 100644 go.sum diff --git a/catalog/client_entities_getbyid.go b/catalog/client_entities_getbyid.go index 7803340..2039255 100644 --- a/catalog/client_entities_getbyid.go +++ b/catalog/client_entities_getbyid.go @@ -35,9 +35,7 @@ func (c *Client) GetEntityByUID(ctx context.Context, request *GetEntityByUIDRequ }); err != nil { return nil, err } - entity := Entity{ - Raw: rawEntity, - } + var entity Entity if err := json.Unmarshal(rawEntity, &entity); err != nil { return nil, err } diff --git a/catalog/client_entities_getbyname.go b/catalog/client_entities_getbyname.go index b084e6a..b16d697 100644 --- a/catalog/client_entities_getbyname.go +++ b/catalog/client_entities_getbyname.go @@ -41,9 +41,7 @@ func (c *Client) GetEntityByName(ctx context.Context, request *GetEntityByNameRe }); err != nil { return nil, err } - entity := Entity{ - Raw: rawEntity, - } + var entity Entity if err := json.Unmarshal(rawEntity, &entity); err != nil { return nil, err } diff --git a/catalog/client_entities_list.go b/catalog/client_entities_list.go index bc0d3a1..0e15808 100644 --- a/catalog/client_entities_list.go +++ b/catalog/client_entities_list.go @@ -55,7 +55,7 @@ func (c *Client) ListEntities(ctx context.Context, request *ListEntitiesRequest) if request.After != "" { query.Set("after", request.After) } - var rawEntities []json.RawMessage + var entities []*Entity var nextPageToken string if err := c.get(ctx, path, query, func(response *http.Response) error { for _, link := range response.Header.Values("link") { @@ -67,22 +67,12 @@ func (c *Client) ListEntities(ctx context.Context, request *ListEntitiesRequest) nextPageToken = linkURL.Query().Get("after") } } - return json.NewDecoder(response.Body).Decode(&rawEntities) + return json.NewDecoder(response.Body).Decode(&entities) }); err != nil { return nil, err } - response := ListEntitiesResponse{ - Entities: make([]*Entity, 0, len(rawEntities)), + return &ListEntitiesResponse{ + Entities: entities, NextPageToken: nextPageToken, - } - for _, rawEntity := range rawEntities { - entity := &Entity{ - Raw: rawEntity, - } - if err := json.Unmarshal(rawEntity, &entity); err != nil { - return nil, err - } - response.Entities = append(response.Entities, entity) - } - return &response, nil + }, nil } diff --git a/catalog/entity.go b/catalog/entity.go index afa4b38..2c582de 100644 --- a/catalog/entity.go +++ b/catalog/entity.go @@ -8,19 +8,38 @@ import ( // An Entity in the software catalog. type Entity struct { // APIVersion is the version of specification format for this particular entity. - APIVersion string `json:"apiVersion"` + APIVersion string // Kind is the high-level entity type. - Kind EntityKind `json:"kind"` + Kind EntityKind // Metadata related to the entity. - Metadata EntityMetadata `json:"metadata"` + Metadata EntityMetadata // Relations that this entity has with other entities. - Relations []EntityRelation `json:"relations,omitempty"` + Relations []EntityRelation // Raw entity JSON message. - Raw json.RawMessage `json:"-"` + Raw json.RawMessage +} + +// UnmarshalJSON implements [json.Unmarshaler]. +func (e *Entity) UnmarshalJSON(data []byte) error { + var fields struct { + APIVersion string `json:"apiVersion"` + Kind EntityKind `json:"kind"` + Metadata EntityMetadata `json:"metadata"` + Relations []EntityRelation `json:"relations,omitempty"` + } + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + e.APIVersion = fields.APIVersion + e.Kind = fields.Kind + e.Metadata = fields.Metadata + e.Relations = fields.Relations + e.Raw = data + return nil } // APISpec decodes the entity's spec as a [APISpec]. diff --git a/catalog/spec_api_test.go b/catalog/spec_api_test.go new file mode 100644 index 0000000..736ff51 --- /dev/null +++ b/catalog/spec_api_test.go @@ -0,0 +1,26 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_APISpec(t *testing.T) { + //nolint: lll + const component = `{"apiVersion":"backstage.io/v1alpha1","kind":"API","metadata":{"name":"artist-api","description":"Retrieve artist details"},"spec":{"type":"openapi","lifecycle":"production","owner":"artist-relations-team","system":"artist-engagement-portal","definition":"openapi: \"3.0.0\"\ninfo:\n version: 1.0.0\n title: Artist API\n license:\n name: MIT\nservers:\n - url: http://artist.spotify.net/v1\npaths:\n /artists:\n get:\n summary: List all artists\n...\n"}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(component), &entity)) + expected := &APISpec{ + Type: "openapi", + Lifecycle: "production", + Owner: "artist-relations-team", + System: "artist-engagement-portal", + //nolint: lll + Definition: "openapi: \"3.0.0\"\ninfo:\n version: 1.0.0\n title: Artist API\n license:\n name: MIT\nservers:\n - url: http://artist.spotify.net/v1\npaths:\n /artists:\n get:\n summary: List all artists\n...\n", + } + actual, err := entity.APISpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_component_test.go b/catalog/spec_component_test.go new file mode 100644 index 0000000..ff3b635 --- /dev/null +++ b/catalog/spec_component_test.go @@ -0,0 +1,24 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_ComponentSpec(t *testing.T) { + //nolint: lll + const component = "{\"apiVersion\":\"backstage.io/v1alpha1\",\"kind\":\"Component\",\"metadata\":{\"name\":\"petstore\",\"description\":\"[The Petstore](http://petstore.example.com) is an example API used to show features of the OpenAPI spec.\\n- First item\\n- Second item\\n\",\"links\":[{\"url\":\"https://github.com/swagger-api/swagger-petstore\",\"title\":\"GitHub Repo\",\"icon\":\"github\"}]},\"spec\":{\"type\":\"service\",\"lifecycle\":\"experimental\",\"owner\":\"team-c\",\"providesApis\":[\"petstore\",\"streetlights\",\"hello-world\"]}}" + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(component), &entity)) + expected := &ComponentSpec{ + Type: "service", + Lifecycle: "experimental", + Owner: "team-c", + ProvidesAPIs: []string{"petstore", "streetlights", "hello-world"}, + } + actual, err := entity.ComponentSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_domain_test.go b/catalog/spec_domain_test.go new file mode 100644 index 0000000..c74f7fd --- /dev/null +++ b/catalog/spec_domain_test.go @@ -0,0 +1,21 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_DomainSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1alpha1","kind":"Domain","metadata":{"name":"playback","description":"Everything related to audio playback"},"spec":{"owner":"user:frank.tiernan"}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + expected := &DomainSpec{ + Owner: "user:frank.tiernan", + } + actual, err := entity.DomainSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_group_test.go b/catalog/spec_group_test.go new file mode 100644 index 0000000..62b4aea --- /dev/null +++ b/catalog/spec_group_test.go @@ -0,0 +1,29 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_GroupSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1alpha1","kind":"Group","metadata":{"name":"infrastructure","description":"The infra business unit"},"spec":{"type":"business-unit","profile":{"displayName":"Infrastructure","email":"infrastructure@example.com","picture":"https://example.com/groups/bu-infrastructure.jpeg"},"parent":"ops","children":["backstage","other"],"members":["jdoe"]}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + expected := &GroupSpec{ + Type: "business-unit", + Profile: Profile{ + DisplayName: "Infrastructure", + Email: "infrastructure@example.com", + Picture: "https://example.com/groups/bu-infrastructure.jpeg", + }, + Parent: "ops", + Children: []string{"backstage", "other"}, + Members: []string{"jdoe"}, + } + actual, err := entity.GroupSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_location_test.go b/catalog/spec_location_test.go new file mode 100644 index 0000000..1323740 --- /dev/null +++ b/catalog/spec_location_test.go @@ -0,0 +1,25 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_LocationSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1alpha1","kind":"Location","metadata":{"name":"org-data"},"spec":{"type":"url","targets":["https://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml","https://github.com/myorg/myproject/org-data-dump/catalog-info-consultants.yaml"]}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + expected := &LocationSpec{ + Type: "url", + Targets: []string{ + "https://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml", + "https://github.com/myorg/myproject/org-data-dump/catalog-info-consultants.yaml", + }, + } + actual, err := entity.LocationSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_resource_test.go b/catalog/spec_resource_test.go new file mode 100644 index 0000000..3212485 --- /dev/null +++ b/catalog/spec_resource_test.go @@ -0,0 +1,23 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_ResourceSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1alpha1","kind":"Resource","metadata":{"name":"artists-db","description":"Stores artist details"},"spec":{"type":"database","owner":"team-a","system":"artist-engagement-portal"}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + expected := &ResourceSpec{ + Type: "database", + Owner: "team-a", + System: "artist-engagement-portal", + } + actual, err := entity.ResourceSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_system.go b/catalog/spec_system.go index 2741b8a..51d6170 100644 --- a/catalog/spec_system.go +++ b/catalog/spec_system.go @@ -7,5 +7,5 @@ type SystemSpec struct { // An entity reference to the owner of the system. This field is required. Owner string `json:"owner"` // An entity reference to the domain that the system belongs to. - Domain string `json:"system,omitempty"` + Domain string `json:"domain,omitempty"` } diff --git a/catalog/spec_system_test.go b/catalog/spec_system_test.go new file mode 100644 index 0000000..55985bc --- /dev/null +++ b/catalog/spec_system_test.go @@ -0,0 +1,22 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_SystemSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1alpha1","kind":"System","metadata":{"name":"podcast","description":"Podcast playback"},"spec":{"owner":"team-b","domain":"playback"}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + expected := &SystemSpec{ + Owner: "team-b", + Domain: "playback", + } + actual, err := entity.SystemSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_template_test.go b/catalog/spec_template_test.go new file mode 100644 index 0000000..6dd0623 --- /dev/null +++ b/catalog/spec_template_test.go @@ -0,0 +1,31 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_TemplateSpec(t *testing.T) { + //nolint: lll + const domain = `{"apiVersion":"backstage.io/v1beta2","kind":"Template","metadata":{"name":"v1beta2-demo","title":"Test Action template","description":"scaffolder v1beta2 template demo"},"spec":{"owner":"backstage/techdocs-core","type":"service","parameters":[{"title":"Fill in some steps","required":["name"],"properties":{"name":{"title":"Name","type":"string","description":"Unique name of the component","ui:autofocus":true,"ui:options":{"rows":5}}}},{"title":"Choose a location","required":["repoUrl"],"properties":{"repoUrl":{"title":"Repository Location","type":"string","ui:field":"RepoUrlPicker","ui:options":{"allowedHosts":["github.com"]}}}}],"steps":[{"id":"fetch-base","name":"Fetch Base","action":"fetch:template","input":{"url":"./template","values":{"name":"{{ parameters.name }}"}}},{"id":"fetch-docs","name":"Fetch Docs","action":"fetch:plain","input":{"targetPath":"./community","url":"https://github.com/backstage/community/tree/main/backstage-community-sessions"}}]}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(domain), &entity)) + //nolint: lll + expected := &TemplateSpec{ + Type: "service", + Owner: "backstage/techdocs-core", + RawParameters: []json.RawMessage{ + json.RawMessage(`{"title":"Fill in some steps","required":["name"],"properties":{"name":{"title":"Name","type":"string","description":"Unique name of the component","ui:autofocus":true,"ui:options":{"rows":5}}}}`), + json.RawMessage(`{"title":"Choose a location","required":["repoUrl"],"properties":{"repoUrl":{"title":"Repository Location","type":"string","ui:field":"RepoUrlPicker","ui:options":{"allowedHosts":["github.com"]}}}}`), + }, + RawSteps: []json.RawMessage{ + json.RawMessage(`{"id":"fetch-base","name":"Fetch Base","action":"fetch:template","input":{"url":"./template","values":{"name":"{{ parameters.name }}"}}}`), + json.RawMessage(`{"id":"fetch-docs","name":"Fetch Docs","action":"fetch:plain","input":{"targetPath":"./community","url":"https://github.com/backstage/community/tree/main/backstage-community-sessions"}}`), + }, + } + actual, err := entity.TemplateSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/catalog/spec_user_test.go b/catalog/spec_user_test.go new file mode 100644 index 0000000..47984bd --- /dev/null +++ b/catalog/spec_user_test.go @@ -0,0 +1,26 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEntity_UserSpec(t *testing.T) { + //nolint: lll + const component = `{"apiVersion":"backstage.io/v1alpha1","kind":"User","metadata":{"name":"jdoe"},"spec":{"profile":{"displayName":"Jenny Doe","email":"jenny-doe@example.com","picture":"https://example.com/staff/jenny-with-party-hat.jpeg"},"memberOf":["team-b","employees"]}}` + var entity Entity + assert.NilError(t, json.Unmarshal([]byte(component), &entity)) + expected := &UserSpec{ + Profile: Profile{ + DisplayName: "Jenny Doe", + Email: "jenny-doe@example.com", + Picture: "https://example.com/staff/jenny-with-party-hat.jpeg", + }, + MemberOf: []string{"team-b", "employees"}, + } + actual, err := entity.UserSpec() + assert.NilError(t, err) + assert.DeepEqual(t, expected, actual) +} diff --git a/cmd/backstage/go.sum b/cmd/backstage/go.sum index 90df82b..293bc60 100644 --- a/cmd/backstage/go.sum +++ b/cmd/backstage/go.sum @@ -3,6 +3,7 @@ github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -21,3 +22,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= diff --git a/go.mod b/go.mod index 215e7f8..dbec91f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module go.einride.tech/backstage go 1.20 + +require gotest.tools/v3 v3.4.0 + +require github.com/google/go-cmp v0.5.5 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6f2155e --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=