From: Bryan C. Mills Date: Wed, 21 Apr 2021 20:04:26 +0000 (-0400) Subject: cmd/go: add tests for convergence in 'go mod tidy' X-Git-Tag: go1.17beta1~439 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=3cc3a16029eea1c3d9fefa77a24a89760c7aa27d;p=gostls13.git cmd/go: add tests for convergence in 'go mod tidy' For #36460 Change-Id: I40194bb1ebab77459e5c9d43bdac4d9c1b826ac2 Reviewed-on: https://go-review.googlesource.com/c/go/+/312449 Trust: Bryan C. Mills Run-TryBot: Bryan C. Mills TryBot-Result: Go Bot Reviewed-by: Michael Matloob --- diff --git a/src/cmd/go/testdata/script/mod_tidy_convergence.txt b/src/cmd/go/testdata/script/mod_tidy_convergence.txt new file mode 100644 index 0000000000..de85d23e5d --- /dev/null +++ b/src/cmd/go/testdata/script/mod_tidy_convergence.txt @@ -0,0 +1,174 @@ +# This test demonstrates a simple case in which 'go mod tidy' may resolve a +# missing package, only to remove that package when resolving its dependencies. +# +# If we naively iterate 'go mod tidy' until the dependency graph converges, this +# scenario may fail to converge. + +# The import graph used in this test looks like: +# +# m --- x +# | +# x_test --- y +# +# The module dependency graph of m is initially empty. +# Modules x and y look like: +# +# x.1 (provides package x that imports y, but does not depend on module y) +# +# x.2-pre (no dependencies, but does not provide package x) +# +# y.1 (no dependencies, but provides package y) +# +# y.2 --- x.2-pre (provides package y) +# +# +# When we resolve the missing import of y in x_test, we add y@latest — which is +# y.2, not y.1 — as a new dependency. That upgrades to x to x.2-pre, which +# removes package x (and also the need for module y). We can then safely remove +# the dependency on module y, because nothing imports package y any more! +# +# We might be tempted to remove the dependency on module x for the same reason: +# it no longer provides any imported package. However, that would cause 'go mod +# tidy -e' to become unstable: with x.2-pre out of the way, we could once again +# resolve the missing import of package x by re-adding x.1. + +cp go.mod go.mod.orig + +# 'go mod tidy' without -e should fail without modifying go.mod, +# because it cannot resolve x and y simultaneously. +! go mod tidy + +cmp go.mod go.mod.orig + +stderr '^go: found example\.net/y in example\.net/y v0.2.0$' +stderr '^go: finding module for package example\.net/x$' + + # TODO: This error message should be clearer — it doesn't indicate why v0.2.0-pre is required. +stderr '^example\.net/m imports\n\texample\.net/x: package example\.net/x provided by example\.net/x at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' + + +# 'go mod tidy -e' should follow upgrades to try to resolve the modules that it +# can, and then stop. When we resolve example.net/y, we upgrade to example.net/x +# to v0.2.0-pre. At that version, package x no longer exists and no longer +# imports package y, so the import of x should be left unsatisfied and the +# existing dependency on example.net/x removed. +# +# TODO(bcmills): It would be ever better if we could keep the original +# dependency on example.net/x v0.1.0, but I don't see a way to do that without +# making the algorithm way too complicated. (We would have to detect that the +# new dependency on example.net/y interferes with the package that caused us to +# to add that dependency in the first place, and back out that part of the change +# without also backing out any other needed changes.) + +go mod tidy -e +cmp go.mod go.mod.tidye +stderr '^go: found example\.net/y in example\.net/y v0.2.0$' + + # TODO: This error message should be clearer — it doesn't indicate why v0.2.0-pre is required. +stderr '^example\.net/m imports\n\texample\.net/x: package example\.net/x provided by example\.net/x at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' + + +# Since we attempt to resolve the dependencies of package x whenever we add x itself, +# this end state is stable. + +go mod tidy -e +cmp go.mod go.mod.tidye + + +# An explicit 'go get' with the correct versions should allow 'go mod tidy' to +# succeed and remain stable. y.1 does not upgrade x, and can therefore be used +# with it. + +go get -d example.net/x@v0.1.0 example.net/y@v0.1.0 +go mod tidy +cmp go.mod go.mod.postget + + + +# TODO(#36460): Repeat this test with a lazy main module. + + +-- go.mod -- +module example.net/m + +go 1.16 + +replace ( + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0 => ./y2 +) + +require ( + example.net/x v0.1.0 +) +-- go.mod.tidye -- +module example.net/m + +go 1.16 + +replace ( + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0 => ./y2 +) +-- go.mod.postget -- +module example.net/m + +go 1.16 + +replace ( + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0 => ./y2 +) + +require ( + example.net/x v0.1.0 + example.net/y v0.1.0 // indirect +) +-- m.go -- +package m + +import _ "example.net/x" + +-- x1/go.mod -- +module example.net/x + +go 1.16 +-- x1/x.go -- +package x +-- x1/x_test.go -- +package x + +import _ "example.net/y" + +-- x2-pre/go.mod -- +module example.net/x + +go 1.16 +-- x2-pre/README.txt -- +There is no package x here. Use example.com/x/subpkg instead. +-- x2-pre/subpkg/subpkg.go -- +package subpkg // import "example.net/x/subpkg" + +-- y1/go.mod -- +module example.net/y + +go 1.16 +-- y1/y.go -- +package y + +-- y2/go.mod -- +module example.net/y + +go 1.16 + +require example.net/x v0.2.0-pre +-- y2/y.go -- +package y + +import _ "example.net/x/subpkg" diff --git a/src/cmd/go/testdata/script/mod_tidy_convergence_loop.txt b/src/cmd/go/testdata/script/mod_tidy_convergence_loop.txt new file mode 100644 index 0000000000..efcd8f2a55 --- /dev/null +++ b/src/cmd/go/testdata/script/mod_tidy_convergence_loop.txt @@ -0,0 +1,272 @@ +# This test demonstrates a simple case in which 'go mod tidy' may resolve a +# missing package, only to remove that package when resolving its dependencies. +# +# If we naively iterate 'go mod tidy' until the dependency graph converges, this +# scenario may fail to converge. + +# The import graph used in this test looks like: +# +# m --- w +# | +# + --- x +# | +# + --- y +# | +# + --- z +# +# The module dependency graph of m initially contains w.1 (and, by extension, +# y.2-pre and z.2-pre). This is an arbitrary point in the cycle of possible +# configurations. +# +# w.1 requires y.2-pre and z.2-pre +# x.1 requires z.2-pre and w.2-pre +# y.1 requires w.2-pre and x.2-pre +# z.1 requires x.2-pre and y.2-pre +# +# At each point, exactly one missing package can be resolved by adding a +# dependency on the .1 release of the module that provides that package. +# However, adding that dependency causes the module providing another package to +# roll over from its .1 release to its .2-pre release, which removes the +# package. Once the package is removed, 'go mod tidy -e' no longer sees the +# module as relevant to the main module, and will happily remove the existing +# dependency on it. +# +# The cycle is of length 4 so that at every step only one package can be +# resolved. This is important because it prevents the iteration from ever +# reaching a state in which every package is simultaneously over-upgraded — such +# a state is stable and does not exhibit failure to converge. + +cp go.mod go.mod.orig + +# 'go mod tidy' without -e should fail without modifying go.mod, +# because it cannot resolve x, y, and z simultaneously. +! go mod tidy + +cmp go.mod go.mod.orig + +stderr '^go: finding module for package example\.net/w$' +stderr '^go: finding module for package example\.net/x$' +stderr -count=2 '^go: finding module for package example\.net/y$' +stderr -count=2 '^go: finding module for package example\.net/z$' +stderr '^go: found example\.net/x in example\.net/x v0.1.0$' + + # TODO: These error messages should be clearer — it doesn't indicate why v0.2.0-pre is required. +stderr '^example\.net/m imports\n\texample\.net/w: package example\.net/w provided by example\.net/w at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' +stderr '^example\.net/m imports\n\texample\.net/y: package example\.net/y provided by example\.net/y at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' +stderr '^example\.net/m imports\n\texample\.net/z: package example\.net/z provided by example\.net/z at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' + + +# 'go mod tidy -e' should preserve all of the upgrades to modules that could +# provide the missing packages but don't. That would at least explain why they +# are missing, and why no individual module can be upgraded in order to satisfy +# a missing import. +# +# TODO(bcmills): Today, it doesn't preserve those upgrades, and instead advances +# the state by one through the cycle of semi-tidy states. + +go mod tidy -e + +cmp go.mod go.mod.tidye1 + +stderr '^go: finding module for package example\.net/w$' +stderr '^go: finding module for package example\.net/x$' +stderr -count=2 '^go: finding module for package example\.net/y$' +stderr -count=2 '^go: finding module for package example\.net/z$' +stderr '^go: found example\.net/x in example\.net/x v0.1.0$' + +stderr '^example\.net/m imports\n\texample\.net/w: package example\.net/w provided by example\.net/w at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' +stderr '^example\.net/m imports\n\texample\.net/y: package example\.net/y provided by example\.net/y at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' +stderr '^example\.net/m imports\n\texample\.net/z: package example\.net/z provided by example\.net/z at latest version v0\.1\.0 but not at required version v0\.2\.0-pre$' + + +go mod tidy -e +cmp go.mod go.mod.tidye2 + +go mod tidy -e +cmp go.mod go.mod.tidye3 + +go mod tidy -e +cmp go.mod go.mod.orig + + +# If we upgrade away all of the packages simultaneously, the resulting tidy +# state converges at "no dependencies", because simultaneously adding all of the +# packages simultaneously over-upgrades all of the dependencies, and 'go mod +# tidy' treats "no package can be added" as a terminal state. + +go get -d example.net/w@v0.2.0-pre example.net/x@v0.2.0-pre example.net/y@v0.2.0-pre example.net/z@v0.2.0-pre +go mod tidy -e +cmp go.mod go.mod.postget +go mod tidy -e +cmp go.mod go.mod.postget + + +# TODO(#36460): Repeat this test with a lazy main module. + + +-- m.go -- +package m + +import ( + _ "example.net/w" + _ "example.net/x" + _ "example.net/y" + _ "example.net/z" +) + +-- go.mod -- +module example.net/m + +go 1.16 + +replace ( + example.net/w v0.1.0 => ./w1 + example.net/w v0.2.0-pre => ./w2-pre + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0-pre => ./y2-pre + example.net/z v0.1.0 => ./z1 + example.net/z v0.2.0-pre => ./z2-pre +) + +require example.net/w v0.1.0 +-- go.mod.tidye1 -- +module example.net/m + +go 1.16 + +replace ( + example.net/w v0.1.0 => ./w1 + example.net/w v0.2.0-pre => ./w2-pre + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0-pre => ./y2-pre + example.net/z v0.1.0 => ./z1 + example.net/z v0.2.0-pre => ./z2-pre +) + +require example.net/x v0.1.0 +-- go.mod.tidye2 -- +module example.net/m + +go 1.16 + +replace ( + example.net/w v0.1.0 => ./w1 + example.net/w v0.2.0-pre => ./w2-pre + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0-pre => ./y2-pre + example.net/z v0.1.0 => ./z1 + example.net/z v0.2.0-pre => ./z2-pre +) + +require example.net/y v0.1.0 +-- go.mod.tidye3 -- +module example.net/m + +go 1.16 + +replace ( + example.net/w v0.1.0 => ./w1 + example.net/w v0.2.0-pre => ./w2-pre + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0-pre => ./y2-pre + example.net/z v0.1.0 => ./z1 + example.net/z v0.2.0-pre => ./z2-pre +) + +require example.net/z v0.1.0 +-- go.mod.postget -- +module example.net/m + +go 1.16 + +replace ( + example.net/w v0.1.0 => ./w1 + example.net/w v0.2.0-pre => ./w2-pre + example.net/x v0.1.0 => ./x1 + example.net/x v0.2.0-pre => ./x2-pre + example.net/y v0.1.0 => ./y1 + example.net/y v0.2.0-pre => ./y2-pre + example.net/z v0.1.0 => ./z1 + example.net/z v0.2.0-pre => ./z2-pre +) +-- w1/go.mod -- +module example.net/w + +go 1.16 + +require ( + example.net/y v0.2.0-pre + example.net/z v0.2.0-pre +) +-- w1/w.go -- +package w +-- w2-pre/go.mod -- +module example.net/w + +go 1.16 +-- w2-pre/README.txt -- +Package w has been removed. + +-- x1/go.mod -- +module example.net/x + +go 1.16 + +require ( + example.net/z v0.2.0-pre + example.net/w v0.2.0-pre +) +-- x1/x.go -- +package x +-- x2-pre/go.mod -- +module example.net/x + +go 1.16 +-- x2-pre/README.txt -- +Package x has been removed. + +-- y1/go.mod -- +module example.net/y + +go 1.16 + +require ( + example.net/w v0.2.0-pre + example.net/x v0.2.0-pre +) +-- y1/y.go -- +package y + +-- y2-pre/go.mod -- +module example.net/y + +go 1.16 +-- y2-pre/README.txt -- +Package y has been removed. + +-- z1/go.mod -- +module example.net/z + +go 1.16 + +require ( + example.net/x v0.2.0-pre + example.net/y v0.2.0-pre +) +-- z1/z.go -- +package z + +-- z2-pre/go.mod -- +module example.net/z + +go 1.16 +-- z2-pre/README.txt -- +Package z has been removed.