marshal - parse_test.go
1 package main
2
3 import (
4 "errors"
5 "go/token"
6 "go/types"
7 "io"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "reflect"
12 "runtime"
13 "slices"
14 "strings"
15 "testing"
16 "time"
17 )
18
19 type testFile string
20
21 func (t testFile) Name() string { return string(t) }
22 func (t testFile) Size() int64 { return -1 }
23 func (t testFile) Mode() fs.FileMode { return fs.ModeType }
24 func (t testFile) ModTime() time.Time { return time.Now() }
25 func (t testFile) IsDir() bool { return false }
26 func (t testFile) Sys() any { return t }
27
28 type testFS map[string]string
29
30 func (t testFS) OpenFile(name string) (io.ReadCloser, error) {
31 contents, ok := t[name]
32 if !ok {
33 return nil, fs.ErrNotExist
34 }
35
36 return io.NopCloser(strings.NewReader(contents)), nil
37 }
38
39 func (t testFS) IsDir(name string) bool {
40 return name == "."
41 }
42
43 func (t testFS) ReadDir(dir string) ([]fs.FileInfo, error) {
44 entries := make([]fs.FileInfo, 0, len(t))
45
46 for name := range t {
47 entries = append(entries, testFile(name))
48 }
49
50 return entries, nil
51 }
52
53 func (t testFS) ReadFile(name string) ([]byte, error) {
54 contents, ok := t[name]
55 if !ok {
56 return nil, fs.ErrNotExist
57 }
58
59 return []byte(contents), nil
60 }
61
62 func TestListFiles(t *testing.T) {
63 badOS := "windows"
64
65 if runtime.GOOS == "windows" {
66 badOS = "darwin"
67 }
68
69 tfs := testFS{
70 "a.go": "package main\n\nconst a = 1",
71 "a_" + badOS + ".go": "package main\n\nconst b = 1",
72 "a_" + runtime.GOOS + ".go": "package main\n\nconst b = 2",
73 "go.mod": "module example.com/main\n\ngo 1.25.5",
74 "a_test.go": "package main\n\nconst c = 3",
75 }
76
77 files, err := listGoFiles(&tfs)
78 if err != nil {
79 t.Fatalf("unexpected error: %s", err)
80 }
81
82 slices.Sort(files)
83
84 expectedFiles := []string{"a.go", "a_" + runtime.GOOS + ".go"}
85
86 if !slices.Equal(files, expectedFiles) {
87 t.Errorf("expecting files %v, got %v", expectedFiles, files)
88 }
89 }
90
91 func TestParsePackage(t *testing.T) {
92 tfs := testFS{
93 "a.go": "package main\n\ntype A struct {B int}",
94 }
95
96 m := moduleDetails{fset: token.NewFileSet()}
97
98 if pkg, err := m.ParsePackage(tfs, "example.com/pkg"); err != nil {
99 t.Errorf("unexpected error: %s", err)
100 } else if a := pkg.Scope().Lookup("A"); a == nil {
101 t.Error("expected type def, got nil")
102 } else if pkgPath := a.Pkg().Path(); pkgPath != "example.com/pkg" {
103 t.Errorf("expecting package path %q, got %q", "example.com/pkg", pkgPath)
104 } else if as, ok := a.Type().Underlying().(*types.Struct); !ok {
105 t.Error("expected struct type")
106 } else if nf := as.NumFields(); nf != 1 {
107 t.Errorf("expected 1 field, got %d", nf)
108 } else if name := as.Field(0).Name(); name != "B" {
109 t.Errorf("expected field name %q, got %q", "B", name)
110 } else if b, ok := as.Field(0).Type().Underlying().(*types.Basic); !ok {
111 t.Error("expected basic type")
112 } else if b.Kind() != types.Int {
113 t.Errorf("expected type %d, got %v", types.Int, b.Kind())
114 }
115
116 if pkg, err := m.ParsePackage(tfs, "", "a.go"); err != nil {
117 t.Errorf("unexpected error: %s", err)
118 } else if a := pkg.Scope().Lookup("A"); a != nil {
119 t.Errorf("expected no object, got %v", a)
120 }
121 }
122
123 func TestParseFiles(t *testing.T) {
124 tfs := testFS{
125 "a.go": "package main\n\ntype A struct {B int}",
126 "b.go": "package pkg\n\ntype D struct {E int}",
127 "c.go": "package pkg\n\ntype F struct {G int}",
128 }
129
130 m := moduleDetails{fset: token.NewFileSet()}
131
132 if files, err := m.parseFiles(".", tfs, []string{"a.go", "b.go", "c.go"}); !errors.Is(err, errMultiplePackages) {
133 t.Errorf("expecting error errMultiplePackages, got %v", err)
134 } else if files != nil {
135 t.Errorf("expecting nil files, got %v", files)
136 } else if files, err = m.parseFiles(".", tfs, []string{"b.go", "c.go"}); err != nil {
137 t.Errorf("unexpected error: %s", err)
138 } else if len(files) != 2 {
139 t.Errorf("expecting 2 files, got %d", len(files))
140 }
141 }
142
143 func TestParseModFile(t *testing.T) {
144 tfs := testFS{
145 "go.mod": `module vimagination.zapto.org/marshal
146
147 go 1.25.5
148
149 require (
150 golang.org/x/mod v0.31.0
151 golang.org/x/tools v0.40.0
152 )
153
154 require golang.org/x/sync v0.19.0 // indirect
155
156 replace golang.org/x/tools => somewhere.org/tools v0.1.0
157 `,
158 }
159
160 if pkg, err := parseModFile(tfs, ""); err != nil {
161 t.Errorf("unexpected error: %s", err)
162 } else if pkg.Module != "vimagination.zapto.org/marshal" {
163 t.Errorf("expecting path %q, got %q", "vimagination.zapto.org/marshal", pkg.Module)
164 } else if len(pkg.Imports) != 3 {
165 t.Errorf("expecting 3 imports, got %d", len(pkg.Imports))
166 } else if m := pkg.Imports["golang.org/x/mod"]; m.Path != "golang.org/x/mod" {
167 t.Errorf("expecting url for %q to be %q, got %q", "golang.org/x/mod", "golang.org/x/mod", m.Path)
168 } else if m.Version != "v0.31.0" {
169 t.Errorf("expecting version for %q to be %q, got %q", "golang.org/x/mod", "v0.31.0", m.Version)
170 } else if m = pkg.Imports["golang.org/x/tools"]; m.Path != "somewhere.org/tools" {
171 t.Errorf("expecting url for %q to be %q, got %q", "golang.org/x/tools", "somewhere.org/tools", m.Path)
172 } else if m.Version != "v0.1.0" {
173 t.Errorf("expecting version for %q to be %q, got %q", "golang.org/x/tools", "v0.1.0", m.Version)
174 }
175 }
176
177 func TestModCacheURL(t *testing.T) {
178 im := importDetails{Base: "golang.org/x/sync", Version: "v0.19.0"}
179 url, err := im.ModCacheURL()
180 if err != nil {
181 t.Errorf("unexpected error: %s", err)
182 } else if url != "https://proxy.golang.org/golang.org/x/sync/@v/v0.19.0.zip" {
183 t.Errorf("expecting URL %q, got %q", "https://proxy.golang.org/golang.org/x/sync/@v/v0.19.0.zip", url)
184 }
185 }
186
187 func TestImportResolve(t *testing.T) {
188 tfs := testFS{
189 "go.mod": `module vimagination.zapto.org/marshal
190
191 go 1.25.5
192
193 require (
194 golang.org/x/mod v0.31.0
195 golang.org/x/tools v0.40.0
196 vimagination.zapto.org/httpreaderat v1.0.0
197 )
198
199 require golang.org/x/sync v0.19.0 // indirect
200
201 replace golang.org/x/tools => somewhere.org/tools v0.1.0
202
203 replace vimagination.zapto.org/httpreaderat v1.0.0 => ../httpreaderat`,
204 }
205
206 mod := importDetails{Base: "golang.org/x/mod", Version: "v0.31.0", Path: "."}
207 modFile := importDetails{Base: "golang.org/x/mod", Version: "v0.31.0", Path: "modfile"}
208 tools := importDetails{Base: "somewhere.org/tools", Version: "v0.1.0", Path: "."}
209 httpreaderat := importDetails{Base: "../httpreaderat", Path: "."}
210 httpreaderatsub := importDetails{Base: "../httpreaderat", Path: "sub"}
211
212 if pkg, err := parseModFile(tfs, ""); err != nil {
213 t.Errorf("unexpected error: %s", err)
214 } else if im := pkg.Resolve("unknown.com/pkg"); im != nil {
215 t.Errorf("expecting nil response, got %v", im)
216 } else if im = pkg.Resolve("golang.org/x/mod"); im != nil && *im != mod {
217 t.Errorf("expecting import %v, got %v", mod, im)
218 } else if im = pkg.Resolve("golang.org/x/mod/modfile"); im != nil && *im != modFile {
219 t.Errorf("expecting import %v, got %v", modFile, im)
220 } else if im = pkg.Resolve("golang.org/x/tools"); im != nil && *im != tools {
221 t.Errorf("expecting import %v, got %v", tools, im)
222 } else if im = pkg.Resolve("vimagination.zapto.org/httpreaderat"); im != nil && *im != httpreaderat {
223 t.Errorf("expecting import %v, got %v", httpreaderat, im)
224 } else if im = pkg.Resolve("vimagination.zapto.org/httpreaderat/sub"); im != nil && *im != httpreaderatsub {
225 t.Errorf("expecting import %v, got %v", httpreaderatsub, im)
226 }
227 }
228
229 func TestAsFS(t *testing.T) {
230 modFile := importDetails{Base: "golang.org/x/mod", Version: "v0.31.0", Path: "modfile"}
231 cache := importDetails{Base: "vimagination.zapto.org/cache", Version: "v1.0.0", Path: "."}
232
233 if f, err := modFile.AsFS(); err != nil {
234 t.Errorf("unexpected error: %s", err)
235 } else if _, err := f.OpenFile("print.go"); err != nil {
236 t.Errorf("unexpected error: %s", err)
237 } else if _, err := f.OpenFile("not-a-file.go"); err == nil {
238 t.Error("expecting error, got nil")
239 } else if _, ok := f.(*zipFS); ok {
240 t.Log("was expecting FS to be a os.DirFS")
241 }
242
243 if f, err := cache.AsFS(); err != nil {
244 t.Errorf("unexpected error: %s", err)
245 } else if mf, err := parseModFile(f, ""); err != nil {
246 t.Errorf("unexpected error: %s", err)
247 } else if mf.Module != cache.Base {
248 t.Errorf("expecting path %q, got %q", cache.Base, mf.Module)
249 } else if _, ok := f.(*zipFS); !ok {
250 t.Log("was expecting FS to be a zipFS")
251 }
252 }
253
254 func TestTypes(t *testing.T) {
255 pkg, err := ParsePackage(".")
256 if err != nil {
257 t.Fatalf("unexpected error: %#v", err)
258 }
259
260 obj := pkg.Scope().Lookup("moduleDetails")
261 if obj == nil {
262 t.Fatal("expecting object, got nil")
263 }
264
265 str, ok := obj.Type().Underlying().(*types.Struct)
266 if !ok {
267 t.Fatal("expecting struct type")
268 }
269
270 typ := reflect.TypeOf(moduleDetails{})
271
272 if str.NumFields() != typ.NumField() {
273 t.Errorf("expecting %d fields, got %d", typ.NumField(), str.NumFields())
274 }
275
276 dir := t.TempDir()
277
278 if err := os.MkdirAll(filepath.Join(dir, "sub"), 0700); err != nil {
279 t.Fatalf("unexpected error: %#v", err)
280 }
281
282 if err := os.WriteFile(filepath.Join(dir, "sub", "a.go"), []byte("package subpkg\n\ntype A struct{\n\tB int\n}"), 0600); err != nil {
283 t.Fatalf("unexpected error: %#v", err)
284 }
285
286 if _, err := ParsePackage(filepath.Join(dir, "sub")); !errors.Is(err, errNoModFile) {
287 t.Errorf("expecting error %q, got: %v", errNoModFile, err)
288 }
289
290 if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module vimagination.zapto.org/somepkg\n\ngo 1.25.5"), 0600); err != nil {
291 t.Fatalf("unexpected error: %#v", err)
292 }
293
294 if pkg, err := ParsePackage(filepath.Join(dir, "sub")); err != nil {
295 t.Errorf("unexpected error: %#v", err)
296 } else if name := pkg.Name(); name != "subpkg" {
297 t.Errorf("expecting package name %q, got %q", "subpkg", name)
298 } else if obj = pkg.Scope().Lookup("A"); obj == nil {
299 t.Errorf("expecting object, got nil")
300 }
301 }
302