1package modcache
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "io/fs"
8 "os"
9 "path/filepath"
10 "sync"
11 "testing"
12
13 "cuelabs.dev/go/oci/ociregistry"
14 "cuelabs.dev/go/oci/ociregistry/ociclient"
15 "github.com/go-quicktest/qt"
16 "golang.org/x/tools/txtar"
17
18 "cuelang.org/go/mod/modregistry"
19 "cuelang.org/go/mod/modregistrytest"
20 "cuelang.org/go/mod/module"
21)
22
23func TestRequirements(t *testing.T) {
24 dir := t.TempDir()
25 ctx := context.Background()
26 registryFS, err := txtar.FS(txtar.Parse([]byte(`
27-- example.com_foo_v0.0.1/cue.mod/module.cue --
28module: "example.com/foo@v0"
29language: version: "v0.8.0"
30deps: {
31 "foo.com/bar/hello@v0": v: "v0.2.3"
32 "bar.com@v0": v: "v0.5.0"
33}
34`)))
35 qt.Assert(t, qt.IsNil(err))
36 r := newRegistry(t, registryFS)
37 wantRequirements := []module.Version{
38 module.MustNewVersion("bar.com", "v0.5.0"),
39 module.MustNewVersion("foo.com/bar/hello", "v0.2.3"),
40 }
41 // Test two concurrent fetches both using the same directory.
42 var wg sync.WaitGroup
43 fetch := func(r ociregistry.Interface) {
44 defer wg.Done()
45 cr, err := New(modregistry.NewClient(r), dir)
46 if !qt.Check(t, qt.IsNil(err)) {
47 return
48 }
49 summary, err := cr.Requirements(ctx, module.MustNewVersion("example.com/foo", "v0.0.1"))
50 if !qt.Check(t, qt.IsNil(err)) {
51 return
52 }
53 if !qt.Check(t, qt.DeepEquals(summary, wantRequirements)) {
54 return
55 }
56 // Fetch again so that we test the in-memory cache-hit path.
57 summary, err = cr.Requirements(ctx, module.MustNewVersion("example.com/foo", "v0.0.1"))
58 if !qt.Check(t, qt.IsNil(err)) {
59 return
60 }
61 if !qt.Check(t, qt.DeepEquals(summary, wantRequirements)) {
62 return
63 }
64 }
65 wg.Add(2)
66 go fetch(r)
67 go fetch(r)
68 wg.Wait()
69
70 // Check that it still functions without a functional registry.
71 wg.Add(1)
72 fetch(nil)
73
74 // Check that the file is stored in the expected place.
75 data, err := os.ReadFile(filepath.Join(dir, "mod/download/example.com/foo/@v/v0.0.1.mod"))
76 qt.Assert(t, qt.IsNil(err))
77 qt.Assert(t, qt.Matches(string(data), `(?s).*module: "example.com/foo@v0".*`))
78}
79
80func TestFetchFromCacheNotFound(t *testing.T) {
81 dir := t.TempDir()
82 t.Cleanup(func() {
83 RemoveAll(dir)
84 })
85 // The cache should never be hit, so just use a nil registry value.
86 // We'll get a panic if it gets used.
87 cr, err := New(nil, dir)
88 qt.Assert(t, qt.IsNil(err))
89 _, err = cr.FetchFromCache(module.MustNewVersion("example.com/foo", "v0.0.1"))
90 qt.Assert(t, qt.Not(qt.IsNil(err)))
91 qt.Assert(t, qt.ErrorIs(err, modregistry.ErrNotFound))
92}
93
94func TestFetch(t *testing.T) {
95 dir := t.TempDir()
96 t.Cleanup(func() {
97 RemoveAll(dir)
98 })
99 ctx := context.Background()
100 registryFS, err := txtar.FS(txtar.Parse([]byte(`
101-- example.com_foo_v0.0.1/cue.mod/module.cue --
102module: "example.com/foo@v0"
103language: version: "v0.8.0"
104deps: {
105 "foo.com/bar/hello@v0": v: "v0.2.3"
106 "bar.com@v0": v: "v0.5.0"
107}
108-- example.com_foo_v0.0.1/example.cue --
109package example
110-- example.com_foo_v0.0.1/x/x.cue --
111package x
112`)))
113 qt.Assert(t, qt.IsNil(err))
114 r := newRegistry(t, registryFS)
115 wantContents, err := txtarContents(fsSub(registryFS, "example.com_foo_v0.0.1"))
116 qt.Assert(t, qt.IsNil(err))
117 checkContents := func(t *testing.T, loc module.SourceLoc) bool {
118 gotContents, err := txtarContents(fsSub(loc.FS, loc.Dir))
119 if !qt.Check(t, qt.IsNil(err)) {
120 return false
121 }
122 if !qt.Check(t, qt.Equals(string(gotContents), string(wantContents))) {
123 return false
124 }
125 // Check that the location can be used to retrieve the OS file path.
126 osrFS, ok := loc.FS.(module.OSRootFS)
127 if !qt.Check(t, qt.IsTrue(ok)) {
128 return false
129 }
130 root := osrFS.OSRoot()
131 if !qt.Check(t, qt.Not(qt.Equals(root, ""))) {
132 return false
133 }
134 // Check that we can access a module file directly.
135 srcPath := filepath.Join(root, loc.Dir, "example.cue")
136 data, err := os.ReadFile(srcPath)
137 qt.Assert(t, qt.IsNil(err))
138 qt.Assert(t, qt.Equals(string(data), "package example\n"))
139 // Check that the actual paths are as expected.
140 qt.Check(t, qt.Equals(srcPath, filepath.Join(dir, "mod", "extract", "example.com", "foo@v0.0.1", "example.cue")))
141 return true
142 }
143 var wg sync.WaitGroup
144 fetch := func(r ociregistry.Interface) {
145 defer wg.Done()
146 cr, err := New(modregistry.NewClient(r), dir)
147 if !qt.Check(t, qt.IsNil(err)) {
148 return
149 }
150 loc, err := cr.Fetch(ctx, module.MustNewVersion("example.com/foo", "v0.0.1"))
151 if !qt.Check(t, qt.IsNil(err)) {
152 return
153 }
154 checkContents(t, loc)
155
156 // After Fetch has succeeded, FetchFromCache should also succeed
157 // and return the same thing.
158 loc, err = cr.FetchFromCache(module.MustNewVersion("example.com/foo", "v0.0.1"))
159 if !qt.Check(t, qt.IsNil(err)) {
160 return
161 }
162 checkContents(t, loc)
163 }
164 wg.Add(2)
165 go fetch(r)
166 go fetch(r)
167 wg.Wait()
168 // Check that it still functions without a functional registry.
169 wg.Add(1)
170 fetch(nil)
171}
172
173func fsSub(fsys fs.FS, sub string) fs.FS {
174 fsys, err := fs.Sub(fsys, sub)
175 if err != nil {
176 panic(err)
177 }
178 return fsys
179}
180
181// txtarContents returns the contents of fsys in txtar format.
182// It assumes that all files end in a newline and do not contain
183// a txtar separator.
184func txtarContents(fsys fs.FS) ([]byte, error) {
185 var buf bytes.Buffer
186 err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
187 if err != nil {
188 return err
189 }
190 if d.IsDir() {
191 return nil
192 }
193 data, err := fs.ReadFile(fsys, path)
194 if err != nil {
195 return err
196 }
197 fmt.Fprintf(&buf, "-- %s --\n", path)
198 buf.Write(data)
199 return nil
200 })
201 return buf.Bytes(), err
202}
203
204func newRegistry(t *testing.T, fsys fs.FS) ociregistry.Interface {
205 regSrv, err := modregistrytest.New(fsys, "")
206 qt.Assert(t, qt.IsNil(err))
207 t.Cleanup(regSrv.Close)
208 regOCI, err := ociclient.New(regSrv.Host(), &ociclient.Options{
209 Insecure: true,
210 })
211 qt.Assert(t, qt.IsNil(err))
212 return regOCI
213}