From 23fec697118a15bc14ba3b46dd999415cb7c1dde Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Thu, 24 Aug 2017 11:06:48 -0500 Subject: [PATCH 1/5] cmd/dep: fix vndrImporter's rev->constraint logic * When a revision is specified, if it's tagged with a semver, use that to default a constraint. * When it's a revision, and we can't guess a constraint, use gps.Any() instead of the revision itself (never pin during import). * Use the standarized validation checks for importers. --- cmd/dep/root_analyzer.go | 16 +++ cmd/dep/testdata/vndr/golden.txt | 4 +- cmd/dep/vndr_importer.go | 53 +++++++--- cmd/dep/vndr_importer_test.go | 168 +++++++++++-------------------- 4 files changed, 118 insertions(+), 123 deletions(-) diff --git a/cmd/dep/root_analyzer.go b/cmd/dep/root_analyzer.go index e8d8b25727..da9d3fdf91 100644 --- a/cmd/dep/root_analyzer.go +++ b/cmd/dep/root_analyzer.go @@ -173,6 +173,22 @@ func (a *rootAnalyzer) Info() gps.ProjectAnalyzerInfo { } } +// isVersion determines if the specified value is a version/tag in the project. +func isVersion(pi gps.ProjectIdentifier, value string, sm gps.SourceManager) (bool, gps.Version, error) { + versions, err := sm.ListVersions(pi) + if err != nil { + return false, nil, errors.Wrapf(err, "list versions for %s(%s)", pi.ProjectRoot, pi.Source) // means repo does not exist + } + + for _, version := range versions { + if value == version.String() { + return true, version, nil + } + } + + return false, nil, nil +} + // lookupVersionForLockedProject figures out the appropriate version for a locked // project based on the locked revision and the constraint from the manifest. // First try matching the revision to a version, then try the constraint from the diff --git a/cmd/dep/testdata/vndr/golden.txt b/cmd/dep/testdata/vndr/golden.txt index a35fdc8669..3702ae436a 100644 --- a/cmd/dep/testdata/vndr/golden.txt +++ b/cmd/dep/testdata/vndr/golden.txt @@ -1,6 +1,6 @@ Detected vndr configuration file... Converting from vendor.conf... - Using 3f4c3bea144e112a69bbe5d8d01c1b09a544253f as initial hint for imported dep github.com/sdboyer/deptest + Using ^0.8.1 as initial constraint for imported dep github.com/sdboyer/deptest Trying v0.8.1 (3f4c3be) as initial lock for imported dep github.com/sdboyer/deptest Using ^2.0.0 as initial constraint for imported dep github.com/sdboyer/deptestdos - Trying * (v2.0.0) as initial lock for imported dep github.com/sdboyer/deptestdos + Trying v2.0.0 (5c60720) as initial lock for imported dep github.com/sdboyer/deptestdos diff --git a/cmd/dep/vndr_importer.go b/cmd/dep/vndr_importer.go index 79eee3f966..3e479922a9 100644 --- a/cmd/dep/vndr_importer.go +++ b/cmd/dep/vndr_importer.go @@ -88,13 +88,28 @@ func (v *vndrImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, er var ( manifest = dep.NewManifest() lock = &dep.Lock{} - err error ) for _, pkg := range v.packages { - // ImportPath must not be empty if pkg.importPath == "" { - err := errors.New("Invalid vndr configuration, missing import path") + err := errors.New("Invalid vndr configuration, import path is required") + return nil, nil, err + } + + // Obtain ProjectRoot. Required for avoiding sub-package imports. + ip, err := v.sm.DeduceProjectRoot(pkg.importPath) + if err != nil { + return nil, nil, err + } + pkg.importPath = string(ip) + + // Check if it already existing in locked projects + if projectExistsInLock(lock, ip) { + continue + } + + if pkg.reference == "" { + err := errors.New("Invalid vndr configuration, revision is required") return nil, nil, err } @@ -103,10 +118,27 @@ func (v *vndrImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, er ProjectRoot: gps.ProjectRoot(pkg.importPath), Source: pkg.repository, }, + Constraint: gps.Any(), } - pc.Constraint, err = v.sm.InferConstraint(pkg.revision, pc.Ident) + + isVersion, version, err := isVersion(pc.Ident, pkg.reference, v.sm) if err != nil { - return nil, nil, errors.Wrapf(err, "Unable to interpret revision specifier '%s' for package %s", pkg.importPath, pkg.revision) + return nil, nil, err + } + + // Check if the revision was tagged with a version + if !isVersion { + revision := gps.Revision(pkg.reference) + version, err = lookupVersionForLockedProject(pc.Ident, nil, revision, v.sm) + if err != nil { + v.logger.Println(err.Error()) + } + } + + // Try to build a constraint from the version + pp := getProjectPropertiesFromVersion(version) + if pp.Constraint != nil { + pc.Constraint = pp.Constraint } manifest.Constraints[pc.Ident.ProjectRoot] = gps.ProjectProperties{ @@ -115,14 +147,7 @@ func (v *vndrImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, er } fb.NewConstraintFeedback(pc, fb.DepTypeImported).LogFeedback(v.logger) - revision := gps.Revision(pkg.revision) - version, err := lookupVersionForLockedProject(pc.Ident, pc.Constraint, revision, v.sm) - if err != nil { - v.logger.Println(err.Error()) - } - lp := gps.NewLockedProject(pc.Ident, version, nil) - lock.P = append(lock.P, lp) fb.NewLockedProjectFeedback(lp, fb.DepTypeImported).LogFeedback(v.logger) } @@ -132,7 +157,7 @@ func (v *vndrImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, er type vndrPackage struct { importPath string - revision string + reference string repository string } @@ -155,7 +180,7 @@ func parseVndrLine(line string) (*vndrPackage, error) { pkg := &vndrPackage{ importPath: parts[0], - revision: parts[1], + reference: parts[1], } if len(parts) == 3 { pkg.repository = parts[2] diff --git a/cmd/dep/vndr_importer_test.go b/cmd/dep/vndr_importer_test.go index d67541fa6c..4447096910 100644 --- a/cmd/dep/vndr_importer_test.go +++ b/cmd/dep/vndr_importer_test.go @@ -19,55 +19,65 @@ import ( func TestVndrConfig_Convert(t *testing.T) { testCases := map[string]struct { - vndr []vndrPackage - wantConvertErr bool - matchPairedVersion bool - projectRoot gps.ProjectRoot - wantConstraint string - wantRevision gps.Revision - wantVersion string - wantLockCount int + *convertTestCase + packages []vndrPackage }{ - "project": { - vndr: []vndrPackage{{ + "semver reference": { + packages: []vndrPackage{{ importPath: "github.com/sdboyer/deptest", - revision: "v0.8.0", + reference: "v0.8.0", repository: "https://github.com/sdboyer/deptest.git", }}, - matchPairedVersion: false, - projectRoot: gps.ProjectRoot("github.com/sdboyer/deptest"), - wantConstraint: "^0.8.0", - wantRevision: gps.Revision("ff2948a2ac8f538c4ecd55962e919d1e13e74baf"), - wantVersion: "v0.8.0", - wantLockCount: 1, + convertTestCase: &convertTestCase{ + projectRoot: gps.ProjectRoot("github.com/sdboyer/deptest"), + wantSourceRepo: "https://github.com/sdboyer/deptest.git", + wantConstraint: "^0.8.0", + wantRevision: gps.Revision("ff2948a2ac8f538c4ecd55962e919d1e13e74baf"), + wantVersion: "v0.8.0", + wantLockCount: 1, + }, }, - "with semver suffix": { - vndr: []vndrPackage{{ + "revision reference": { + packages: []vndrPackage{{ importPath: "github.com/sdboyer/deptest", - revision: "v1.12.0-12-g2fd980e", + reference: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", }}, - matchPairedVersion: false, - projectRoot: gps.ProjectRoot("github.com/sdboyer/deptest"), - wantConstraint: "^1.12.0-12-g2fd980e", - wantVersion: "v1.0.0", - wantLockCount: 1, + convertTestCase: &convertTestCase{ + projectRoot: gps.ProjectRoot("github.com/sdboyer/deptest"), + wantConstraint: "^1.0.0", + wantVersion: "v1.0.0", + wantRevision: gps.Revision("ff2948a2ac8f538c4ecd55962e919d1e13e74baf"), + wantLockCount: 1, + }, }, - "hash revision": { - vndr: []vndrPackage{{ - importPath: "github.com/sdboyer/deptest", - revision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", + "untagged revision reference": { + packages: []vndrPackage{{ + importPath: "github.com/carolynvs/deptest-subpkg", + reference: "6c41d90f78bb1015696a2ad591debfa8971512d5", }}, - matchPairedVersion: false, - projectRoot: gps.ProjectRoot("github.com/sdboyer/deptest"), - wantConstraint: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", - wantVersion: "v1.0.0", - wantLockCount: 1, + convertTestCase: &convertTestCase{ + projectRoot: gps.ProjectRoot("github.com/carolynvs/deptest-subpkg"), + wantConstraint: "*", + wantVersion: "", + wantRevision: gps.Revision("6c41d90f78bb1015696a2ad591debfa8971512d5"), + wantLockCount: 1, + }, }, "missing importPath": { - vndr: []vndrPackage{{ - revision: "v1.0.0", + packages: []vndrPackage{{ + reference: "v1.0.0", + }}, + convertTestCase: &convertTestCase{ + wantConvertErr: true, + }, + }, + "missing reference": { + packages: []vndrPackage{{ + importPath: "github.com/sdboyer/deptest", }}, - wantConvertErr: true, + convertTestCase: &convertTestCase{ + wantConvertErr: true, + }, }, } @@ -81,72 +91,16 @@ func TestVndrConfig_Convert(t *testing.T) { for name, testCase := range testCases { t.Run(name, func(t *testing.T) { - v := newVndrImporter(discardLogger, true, sm) - v.packages = testCase.vndr + g := newVndrImporter(discardLogger, true, sm) + g.packages = testCase.packages - manifest, lock, err := v.convert(testCase.projectRoot) + manifest, lock, convertErr := g.convert(testCase.projectRoot) + err = validateConvertTestCase(testCase.convertTestCase, manifest, lock, convertErr) if err != nil { - if testCase.wantConvertErr { - return - } - t.Fatal(err) - } else { - if testCase.wantConvertErr { - t.Fatal("expected err, have nil") - } - } - - if len(lock.P) != testCase.wantLockCount { - t.Fatalf("Expected lock to have %d project(s), got %d", - testCase.wantLockCount, - len(lock.P)) - } - - d, ok := manifest.Constraints[testCase.projectRoot] - if !ok { - t.Fatalf("Expected the manifest to have a dependency for '%s' but got none", - testCase.projectRoot) - } - - c := d.Constraint.String() - if c != testCase.wantConstraint { - t.Fatalf("Expected manifest constraint to be %s, got %s", testCase.wantConstraint, c) - } - - p := lock.P[0] - if p.Ident().ProjectRoot != testCase.projectRoot { - t.Fatalf("Expected the lock to have a project for '%s' but got '%s'", - testCase.projectRoot, - p.Ident().ProjectRoot) - } - - lv := p.Version() - lpv, ok := lv.(gps.PairedVersion) - - if !ok { - if testCase.matchPairedVersion { - t.Fatalf("Expected locked version to be PairedVersion but got %T", lv) - } - - return - } - - ver := lpv.String() - if ver != testCase.wantVersion { - t.Fatalf("Expected locked version to be '%s', got %s", testCase.wantVersion, ver) - } - - if testCase.wantRevision != "" { - rev := lpv.Revision() - if rev != testCase.wantRevision { - t.Fatalf("Expected locked revision to be '%s', got %s", - testCase.wantRevision, - rev) - } + t.Fatalf("%#v", err) } }) } - } func TestVndrConfig_Import(t *testing.T) { @@ -173,15 +127,15 @@ func TestVndrConfig_Import(t *testing.T) { m, l, err := v.Import(projectRoot, testProjectRoot) h.Must(err) - constraint, err := gps.NewSemverConstraint("^2.0.0") - h.Must(err) wantM := dep.NewManifest() + c1, _ := gps.NewSemverConstraint("^0.8.1") wantM.Constraints["github.com/sdboyer/deptest"] = gps.ProjectProperties{ Source: "https://github.com/sdboyer/deptest.git", - Constraint: gps.Revision("3f4c3bea144e112a69bbe5d8d01c1b09a544253f"), + Constraint: c1, } + c2, _ := gps.NewSemverConstraint("^2.0.0") wantM.Constraints["github.com/sdboyer/deptestdos"] = gps.ProjectProperties{ - Constraint: constraint, + Constraint: c2, } if !reflect.DeepEqual(wantM, m) { t.Errorf("unexpected manifest\nhave=%+v\nwant=%+v", m, wantM) @@ -194,14 +148,14 @@ func TestVndrConfig_Import(t *testing.T) { ProjectRoot: "github.com/sdboyer/deptest", Source: "https://github.com/sdboyer/deptest.git", }, - gps.NewVersion("v0.8.1").Pair(gps.Revision("3f4c3bea144e112a69bbe5d8d01c1b09a544253f")), + gps.NewVersion("v0.8.1").Pair("3f4c3bea144e112a69bbe5d8d01c1b09a544253f"), nil, ), gps.NewLockedProject( gps.ProjectIdentifier{ ProjectRoot: "github.com/sdboyer/deptestdos", }, - gps.Revision("v2.0.0"), + gps.NewVersion("v2.0.0").Pair("5c607206be5decd28e6263ffffdcee067266015e"), nil, ), }, @@ -263,14 +217,14 @@ func TestParseVndrLine(t *testing.T) { testcase("github.com/golang/notreal v1.0.0", &vndrPackage{ importPath: "github.com/golang/notreal", - revision: "v1.0.0", + reference: "v1.0.0", }, nil)) t.Run("with repo", testcase("github.com/golang/notreal v1.0.0 https://github.com/golang/notreal", &vndrPackage{ importPath: "github.com/golang/notreal", - revision: "v1.0.0", + reference: "v1.0.0", repository: "https://github.com/golang/notreal", }, nil)) @@ -278,7 +232,7 @@ func TestParseVndrLine(t *testing.T) { testcase("github.com/golang/notreal v1.0.0 https://github.com/golang/notreal # cool comment", &vndrPackage{ importPath: "github.com/golang/notreal", - revision: "v1.0.0", + reference: "v1.0.0", repository: "https://github.com/golang/notreal", }, nil)) From cc15e54175b6333afab5d9a8b452ee7f7bb5eac6 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Thu, 24 Aug 2017 11:25:38 -0500 Subject: [PATCH 2/5] cmd/dep: validate every field during importer tests Always specify each field in the manifest/lock, so that it's easier to tell that a test satisfies all validations. --- cmd/dep/glide_importer_test.go | 2 ++ cmd/dep/godep_importer_test.go | 2 ++ cmd/dep/root_analyzer_test.go | 37 +++++++++++++++++----------------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/cmd/dep/glide_importer_test.go b/cmd/dep/glide_importer_test.go index 5e6b507055..0b5fa5f0b7 100644 --- a/cmd/dep/glide_importer_test.go +++ b/cmd/dep/glide_importer_test.go @@ -88,6 +88,7 @@ func TestGlideConfig_Convert(t *testing.T) { wantLockCount: 1, wantConstraint: "^1.0.0", wantVersion: "v1.0.0", + wantRevision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", }, }, "revision only": { @@ -110,6 +111,7 @@ func TestGlideConfig_Convert(t *testing.T) { projectRoot: "github.com/sdboyer/deptest", wantLockCount: 1, wantRevision: gps.Revision("ff2948a2ac8f538c4ecd55962e919d1e13e74baf"), + wantVersion: "v1.0.0", }, }, "with ignored package": { diff --git a/cmd/dep/godep_importer_test.go b/cmd/dep/godep_importer_test.go index 08c3e5018e..6875363c35 100644 --- a/cmd/dep/godep_importer_test.go +++ b/cmd/dep/godep_importer_test.go @@ -56,6 +56,7 @@ func TestGodepConfig_Convert(t *testing.T) { wantConstraint: "^1.12.0-12-g2fd980e", wantLockCount: 1, wantVersion: "v1.0.0", + wantRevision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", }, }, "empty comment": { @@ -116,6 +117,7 @@ func TestGodepConfig_Convert(t *testing.T) { wantLockCount: 1, wantConstraint: "^1.0.0", wantVersion: "v1.0.0", + wantRevision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", }, }, } diff --git a/cmd/dep/root_analyzer_test.go b/cmd/dep/root_analyzer_test.go index 173b2e06bd..9170cfe6a1 100644 --- a/cmd/dep/root_analyzer_test.go +++ b/cmd/dep/root_analyzer_test.go @@ -229,26 +229,27 @@ func validateConvertTestCase(testCase *convertTestCase, manifest *dep.Manifest, return errors.Errorf("Expected locked source to be %s, got '%s'", testCase.wantSourceRepo, p.Ident().Source) } - if testCase.wantVersion != "" { - ver := p.Version().String() - if ver != testCase.wantVersion { - return errors.Errorf("Expected locked version to be '%s', got %s", testCase.wantVersion, ver) - } + // Break down the locked "version" into a version (optional) and revision + var gotVersion string + var gotRevision gps.Revision + if lpv, ok := p.Version().(gps.PairedVersion); ok { + gotVersion = lpv.String() + gotRevision = lpv.Revision() + } else if lr, ok := p.Version().(gps.Revision); ok { + gotRevision = lr + } else { + return errors.New("could not determine the type of the locked version") } - if testCase.wantRevision != "" { - lv := p.Version() - lpv, ok := lv.(gps.PairedVersion) - if !ok { - return errors.Errorf("Expected locked version to be PairedVersion but got %T", lv) - } - - rev := lpv.Revision() - if rev != testCase.wantRevision { - return errors.Errorf("Expected locked revision to be '%s', got %s", - testCase.wantRevision, - rev) - } + if gotRevision != testCase.wantRevision { + return errors.Errorf("Expected locked revision to be '%s', got %s", + testCase.wantRevision, + gotRevision) + } + if gotVersion != testCase.wantVersion { + return errors.Errorf("Expected locked version to be '%s', got %s", + testCase.wantVersion, + gotVersion) } } return nil From ab098c65acc36c0f8015e2f8c5fad60c93a808e7 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Fri, 25 Aug 2017 10:07:05 -0500 Subject: [PATCH 3/5] isVersion should not return true for a branch --- cmd/dep/root_analyzer.go | 4 ++++ cmd/dep/root_analyzer_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cmd/dep/root_analyzer.go b/cmd/dep/root_analyzer.go index da9d3fdf91..b5a48e4073 100644 --- a/cmd/dep/root_analyzer.go +++ b/cmd/dep/root_analyzer.go @@ -181,6 +181,10 @@ func isVersion(pi gps.ProjectIdentifier, value string, sm gps.SourceManager) (bo } for _, version := range versions { + if version.Type() != gps.IsVersion && version.Type() != gps.IsSemver { + continue + } + if value == version.String() { return true, version, nil } diff --git a/cmd/dep/root_analyzer_test.go b/cmd/dep/root_analyzer_test.go index 9170cfe6a1..6c8d8a03f8 100644 --- a/cmd/dep/root_analyzer_test.go +++ b/cmd/dep/root_analyzer_test.go @@ -154,6 +154,41 @@ func TestProjectExistsInLock(t *testing.T) { } } +func TestIsVersion(t *testing.T) { + testcases := map[string]struct { + wantIsVersion bool + wantVersion gps.Version + }{ + "v1.0.0": {wantIsVersion: true, wantVersion: gps.NewVersion("v1.0.0").Pair("ff2948a2ac8f538c4ecd55962e919d1e13e74baf")}, + "3f4c3bea144e112a69bbe5d8d01c1b09a544253f": {wantIsVersion: false}, + "master": {wantIsVersion: false}, + } + + pi := gps.ProjectIdentifier{ProjectRoot: gps.ProjectRoot("github.com/sdboyer/deptest")} + h := test.NewHelper(t) + defer h.Cleanup() + + ctx := newTestContext(h) + sm, err := ctx.SourceManager() + h.Must(err) + defer sm.Release() + + for value, testcase := range testcases { + t.Run(value, func(t *testing.T) { + gotIsVersion, gotVersion, err := isVersion(pi, value, sm) + h.Must(err) + + if testcase.wantIsVersion != gotIsVersion { + t.Fatalf("Expected isVersion for %s to be %t", value, testcase.wantIsVersion) + } + + if testcase.wantVersion != gotVersion { + t.Fatalf("Expected version for %s to be %s, got %s", value, testcase.wantVersion, gotVersion) + } + }) + } +} + // convertTestCase is a common set of validations applied to the result // of an importer converting from an external config format to dep's. type convertTestCase struct { From ce8bb4a78ffda00a9a032ad9742cdc2930dacdb8 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Fri, 25 Aug 2017 10:07:47 -0500 Subject: [PATCH 4/5] Clarify that vndr references could be a rev or tag --- cmd/dep/vndr_importer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/dep/vndr_importer.go b/cmd/dep/vndr_importer.go index 3e479922a9..01463ca536 100644 --- a/cmd/dep/vndr_importer.go +++ b/cmd/dep/vndr_importer.go @@ -121,12 +121,13 @@ func (v *vndrImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, er Constraint: gps.Any(), } + // A vndr entry could contain either a version or a revision isVersion, version, err := isVersion(pc.Ident, pkg.reference, v.sm) if err != nil { return nil, nil, err } - // Check if the revision was tagged with a version + // If the reference is a revision, check if it is tagged with a version if !isVersion { revision := gps.Revision(pkg.reference) version, err = lookupVersionForLockedProject(pc.Ident, nil, revision, v.sm) From 1bb5f29d0f81022c95700608a0019bca9110a33f Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Mon, 28 Aug 2017 22:19:46 -0500 Subject: [PATCH 5/5] Switch to standard GOT/WNT error formatting --- cmd/dep/root_analyzer_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/dep/root_analyzer_test.go b/cmd/dep/root_analyzer_test.go index 6c8d8a03f8..0a4e47be3f 100644 --- a/cmd/dep/root_analyzer_test.go +++ b/cmd/dep/root_analyzer_test.go @@ -179,11 +179,11 @@ func TestIsVersion(t *testing.T) { h.Must(err) if testcase.wantIsVersion != gotIsVersion { - t.Fatalf("Expected isVersion for %s to be %t", value, testcase.wantIsVersion) + t.Fatalf("unexpected isVersion result for %s: \n\t(GOT) %v \n\t(WNT) %v", value, gotIsVersion, testcase.wantIsVersion) } if testcase.wantVersion != gotVersion { - t.Fatalf("Expected version for %s to be %s, got %s", value, testcase.wantVersion, gotVersion) + t.Fatalf("unexpected version for %s: \n\t(GOT) %v \n\t(WNT) %v", value, testcase.wantVersion, gotVersion) } }) } @@ -277,12 +277,12 @@ func validateConvertTestCase(testCase *convertTestCase, manifest *dep.Manifest, } if gotRevision != testCase.wantRevision { - return errors.Errorf("Expected locked revision to be '%s', got %s", + return errors.Errorf("unexpected locked revision : \n\t(GOT) %v \n\t(WNT) %v", testCase.wantRevision, gotRevision) } if gotVersion != testCase.wantVersion { - return errors.Errorf("Expected locked version to be '%s', got %s", + return errors.Errorf("unexpected locked version: \n\t(GOT) %v \n\t(WNT) %v", testCase.wantVersion, gotVersion) }