1package modpkgload
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "path"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/go-quicktest/qt"
14 "github.com/google/go-cmp/cmp"
15 "golang.org/x/tools/txtar"
16
17 "cuelang.org/go/cue/ast"
18 "cuelang.org/go/internal/mod/modimports"
19 "cuelang.org/go/internal/mod/modrequirements"
20 "cuelang.org/go/mod/modfile"
21 "cuelang.org/go/mod/module"
22)
23
24func TestLoadPackages(t *testing.T) {
25 files, err := filepath.Glob("testdata/*.txtar")
26 qt.Assert(t, qt.IsNil(err))
27 for _, f := range files {
28 ar, err := txtar.ParseFile(f)
29 qt.Assert(t, qt.IsNil(err))
30 tfs, err := txtar.FS(ar)
31 qt.Assert(t, qt.IsNil(err))
32 reg := testRegistry{tfs}
33 testDirs, _ := fs.Glob(tfs, "test[0-9]*")
34 for _, testDir := range testDirs {
35 testName := strings.TrimSuffix(filepath.Base(f), ".txtar") + "/" + testDir
36 t.Run(testName, func(t *testing.T) {
37 t.Logf("test file: %v", f)
38 readTestFile := func(name string) string {
39 data, err := fs.ReadFile(tfs, path.Join(testDir, name))
40 qt.Assert(t, qt.IsNil(err))
41 return string(data)
42 }
43
44 initialRequirementsStr := strings.Fields(readTestFile("initial-requirements"))
45 mainModulePath, moduleVersions := initialRequirementsStr[0], mapSlice(initialRequirementsStr[1:], module.MustParseVersion)
46 defaultMajorVersions := make(map[string]string)
47 for f := range strings.FieldsSeq(readTestFile("default-major-versions")) {
48 p, v, ok := strings.Cut(f, "@")
49 qt.Assert(t, qt.IsTrue(ok))
50 defaultMajorVersions[p] = v
51 }
52 initialRequirements := modrequirements.NewRequirements(mainModulePath, reg, moduleVersions, defaultMajorVersions)
53
54 rootPackages := strings.Fields(readTestFile("root-packages"))
55 want := readTestFile("want")
56
57 var out strings.Builder
58 printf := func(f string, a ...any) {
59 fmt.Fprintf(&out, f, a...)
60 }
61 pkgs := LoadPackages(
62 context.Background(),
63 mainModulePath,
64 module.SourceLoc{FS: tfs, Dir: "."},
65 initialRequirements,
66 reg,
67 rootPackages,
68 func(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) bool {
69 return true
70 },
71 )
72 for _, pkg := range pkgs.All() {
73 printf("%s\n", pkg.ImportPath())
74 printf("\tflags: %v\n", pkg.Flags())
75 if pkg.Error() != nil {
76 printf("\terror: %v\n", pkg.Error())
77 printf("\tmissing: %v\n", errors.As(pkg.Error(), new(*ImportMissingError)))
78 } else {
79 printf("\tmod: %v\n", pkg.Mod())
80 printf("\texternal: %v\n", pkg.FromExternalModule())
81 // Sanity check that the module file is available at pkg.ModRoot.
82 _, err := fs.Stat(pkg.ModRoot().FS, path.Join(pkg.ModRoot().Dir, "cue.mod/module.cue"))
83 qt.Assert(t, qt.IsNil(err), qt.Commentf("pkg %q; mod root: %#v", pkg.ImportPath(), pkg.ModRoot()))
84 for _, loc := range pkg.Locations() {
85 printf("\tlocation: %v\n", loc.Dir)
86 }
87 for _, file := range pkg.Files() {
88 printf("\tfile: %v: %v\n", file.FilePath, file.Syntax.PackageName())
89 }
90 if imps := pkg.Imports(); len(imps) > 0 {
91 printf("\timports:\n")
92 for _, imp := range imps {
93 printf("\t\t%v\n", imp.ImportPath())
94 }
95 }
96 }
97 }
98 if diff := cmp.Diff(want, out.String()); diff != "" {
99 t.Logf("actual result:\n%s", out.String())
100 t.Fatalf("unexpected results (-want +got):\n%s", diff)
101 }
102 })
103 }
104 }
105}
106
107func TestFindPackageLocations(t *testing.T) {
108 versionForModule := func(ctx context.Context, prefixPath string) (module.Version, error) {
109 t.Logf("versionForModule %q", prefixPath)
110 switch prefixPath {
111 case "foo.bar":
112 return module.Version{}, nil
113 case "foo.bar/a":
114 return module.MustNewVersion("foo.bar/a@v1", "v1.2.3"), nil
115 case "foo.bar/a/b":
116 return module.MustNewVersion("foo.bar/a/b@v0", "v0.2.4"), nil
117 case "foo.bar/a/b/c":
118 return module.MustNewVersion("foo.bar/a/b/c@v0", "v0.3.6"), nil
119 case "foo.bar/a/b/c/d":
120 return module.MustNewVersion("foo.bar/a/b/c/d@v5", "v5.10.20"), nil
121 default:
122 t.Errorf("unexpected call to versionForModule with prefix %q", prefixPath)
123 return module.Version{}, fmt.Errorf("no version")
124 }
125 }
126 tfs, err := txtar.FS(txtar.Parse([]byte(`
127-- foo.bar_a/b/c/cue.mod/module.cue --
128// This should cause foo.bar/a to be excluded from the list
129// of possible candidates because c is a nested module.
130module: "something"
131-- foo.bar_a/b/c/d/x.cue --
132package d
133-- foo.bar_a_b/c/d/x.cue --
134package C
135-- foo.bar_a_b_c/d/x.cue --
136package C
137-- foo.bar_a_b_c_d/x.cue --
138package C
139`)))
140 qt.Assert(t, qt.IsNil(err))
141 fetch := func(ctx context.Context, m module.Version) (loc module.SourceLoc, isLocal bool, err error) {
142 t.Logf("fetch %v", m)
143 switch m.String() {
144 case "foo.bar/a@v1.2.3":
145 // Note: return true for isLocal to trigger the nested module
146 // checking logic.
147 return module.SourceLoc{
148 FS: tfs,
149 Dir: "foo.bar_a",
150 }, true, nil
151 case "foo.bar/a/b@v0.2.4":
152 return module.SourceLoc{
153 FS: tfs,
154 Dir: "foo.bar_a_b",
155 }, false, nil
156 case "foo.bar/a/b/c@v0.3.6":
157 return module.SourceLoc{
158 FS: tfs,
159 Dir: "foo.bar_a_b_c",
160 }, false, nil
161 case "foo.bar/a/b/c/d@v5.10.20":
162 return module.SourceLoc{
163 FS: tfs,
164 Dir: "foo.bar_a_b_c_d",
165 }, false, nil
166 default:
167 t.Errorf("unexpected call to versionForModule with module %q", m)
168 return module.SourceLoc{}, false, fmt.Errorf("no module")
169 }
170 }
171 locs, err := FindPackageLocations(context.Background(), "foo.bar/a/b/c/d", versionForModule, fetch)
172 qt.Assert(t, qt.IsNil(err))
173 var dirs []string
174 for _, loc := range locs {
175 dirs = append(dirs, loc.Locs[0].Dir)
176 }
177 qt.Assert(t, qt.DeepEquals(dirs, []string{
178 "foo.bar_a_b_c_d",
179 "foo.bar_a_b_c/d",
180 "foo.bar_a_b/c/d",
181 }))
182}
183
184type testRegistry struct {
185 fs fs.FS
186}
187
188func (r testRegistry) Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error) {
189 mpath := r.modpath(m)
190 info, err := fs.Stat(r.fs, mpath)
191 if err != nil || !info.IsDir() {
192 return module.SourceLoc{}, fmt.Errorf("module %v not found at %v", m, mpath)
193 }
194 return module.SourceLoc{
195 FS: r.fs,
196 Dir: mpath,
197 }, nil
198}
199
200func (r testRegistry) Requirements(ctx context.Context, m module.Version) ([]module.Version, error) {
201 mpath := path.Join(r.modpath(m), "cue.mod/module.cue")
202 data, err := fs.ReadFile(r.fs, mpath)
203 if err != nil {
204 return nil, err
205 }
206 mf, err := modfile.Parse(data, mpath)
207 if err != nil {
208 return nil, fmt.Errorf("cannot parse module file from %v: %v", m, err)
209 }
210 return mf.DepVersions(), nil
211}
212
213func (r testRegistry) modpath(m module.Version) string {
214 mpath, _, _ := ast.SplitPackageVersion(m.Path())
215 return path.Join("_registry", strings.ReplaceAll(mpath, "/", "_")+"_"+m.Version())
216}
217
218func mapSlice[From, To any](ss []From, f func(From) To) []To {
219 ts := make([]To, len(ss))
220 for i := range ss {
221 ts[i] = f(ss[i])
222 }
223 return ts
224}