# 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 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 # The 'tidy' logic for a lazy main module requires more iterations to converge, # because it is willing to drop dependencies on non-root modules that do not # otherwise provide imported packages. # # On the first iteration, it adds x.1 as a root, which upgrades z and w, # dropping w.1's requirement on y. w.1 was initially a root, so the upgraded # w.2-pre is retained as a root. # # On the second iteration, it adds y.1 as a root, which upgrades w and x, # dropping x.1's requirement on z. x.1 was added as a root in the previous step, # so the upgraded x.2-pre is retained as a root. # # On the third iteration, it adds z.1 as a root, which upgrades x and y. # x and y were already roots (from the previous steps), so their upgraded versions # are retained (not dropped) and the iteration stops. # # At that point, we have z.1 as a root providing package z, # and w, x, and y have all been upgraded to no longer provide any packages. # So only z is retained as a new root. # # (From the above, we can see that in a lazy module we still cycle through the # same possible root states, but in a different order from the eager case.) # # TODO(bcmills): if we retained the upgrades on w, x, and y (since they are # lexical prefixes for unresolved packages w, x, and y, respectively), then 'go # mod tidy -e' itself would become stable and no longer cycle through states. cp go.mod.orig go.mod go mod edit -go=1.17 go.mod cp go.mod go.mod.117 go mod edit -go=1.17 go.mod.tidye1 go mod edit -go=1.17 go.mod.tidye2 go mod edit -go=1.17 go.mod.tidye3 go mod edit -go=1.17 go.mod.postget go list -m all go mod tidy -e cmp go.mod go.mod.tidye3 go mod tidy -e cmp go.mod go.mod.tidye2 go mod tidy -e cmp go.mod go.mod.tidye1 go mod tidy -e cmp go.mod go.mod.117 # As in the eager case, for the lazy module the fully-upgraded dependency graph # becomes empty, and the empty graph is stable. go get 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 -- 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.