1
2
3
4
5 package main
6
7 import (
8 "bytes"
9 "context"
10 "flag"
11 "fmt"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "strconv"
16 "strings"
17 "sync"
18 "testing"
19
20 "golang.org/x/mod/module"
21 "golang.org/x/tools/txtar"
22 )
23
24 var (
25 testwork = flag.Bool("testwork", false, "preserve work directory")
26 updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing")
27 )
28
29 var hasGitCache struct {
30 once sync.Once
31 found bool
32 }
33
34
35 func hasGit() bool {
36 hasGitCache.once.Do(func() {
37 if _, err := exec.LookPath("git"); err != nil {
38 return
39 }
40 hasGitCache.found = true
41 })
42 return hasGitCache.found
43 }
44
45
46
47
48
49
50
51
52
53
54 func prepareProxy(proxyVersions map[module.Version]bool, tests []*test) (ctx context.Context, cleanup func(), _ error) {
55 env := append(os.Environ(), "GO111MODULE=on", "GOSUMDB=off")
56
57 proxyDir, proxyURL, err := buildProxyDir(proxyVersions, tests)
58 if err != nil {
59 return nil, nil, fmt.Errorf("error building proxy dir: %v", err)
60 }
61 env = append(env, fmt.Sprintf("GOPROXY=%s", proxyURL))
62
63 cacheDir, err := os.MkdirTemp("", "gorelease_test-gocache")
64 if err != nil {
65 return nil, nil, err
66 }
67 env = append(env, fmt.Sprintf("GOPATH=%s", cacheDir))
68
69 return context.WithValue(context.Background(), "env", env), func() {
70 if *testwork {
71 fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
72 fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
73 } else {
74 cmd := exec.Command("go", "clean", "-modcache")
75 cmd.Env = env
76 if err := cmd.Run(); err != nil {
77 fmt.Fprintln(os.Stderr, fmt.Errorf("error running go clean: %v", err))
78 }
79
80 if err := os.RemoveAll(cacheDir); err != nil {
81 fmt.Fprintln(os.Stderr, fmt.Errorf("error removing cache dir %s: %v", cacheDir, err))
82 }
83 if err := os.RemoveAll(proxyDir); err != nil {
84 fmt.Fprintln(os.Stderr, fmt.Errorf("error removing proxy dir %s: %v", proxyDir, err))
85 }
86 }
87 }, nil
88 }
89
90
91
92
93
94
95
96
97
98
99
100
101 type test struct {
102 txtar.Archive
103
104
105 testPath string
106
107
108
109 modPath string
110
111
112
113
114 version string
115
116
117
118 baseVersion string
119
120
121
122 releaseVersion string
123
124
125
126
127 dir string
128
129
130
131 wantError bool
132
133
134
135 wantSuccess bool
136
137
138 skip string
139
140
141 want []byte
142
143
144
145
146
147
148 proxyVersions map[module.Version]bool
149
150
151
152 vcs string
153 }
154
155
156 func readTest(testPath string) (*test, error) {
157 arc, err := txtar.ParseFile(testPath)
158 if err != nil {
159 return nil, err
160 }
161 t := &test{
162 Archive: *arc,
163 testPath: testPath,
164 wantSuccess: true,
165 }
166
167 for n, line := range bytes.Split(t.Comment, []byte("\n")) {
168 lineNum := n + 1
169 if i := bytes.IndexByte(line, '#'); i >= 0 {
170 line = line[:i]
171 }
172 line = bytes.TrimSpace(line)
173 if len(line) == 0 {
174 continue
175 }
176
177 var key, value string
178 if i := bytes.IndexByte(line, '='); i < 0 {
179 return nil, fmt.Errorf("%s:%d: no '=' found", testPath, lineNum)
180 } else {
181 key = strings.TrimSpace(string(line[:i]))
182 value = strings.TrimSpace(string(line[i+1:]))
183 }
184 switch key {
185 case "mod":
186 t.modPath = value
187 case "version":
188 t.version = value
189 case "base":
190 t.baseVersion = value
191 case "release":
192 t.releaseVersion = value
193 case "dir":
194 t.dir = value
195 case "skip":
196 t.skip = value
197 case "success":
198 t.wantSuccess, err = strconv.ParseBool(value)
199 if err != nil {
200 return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
201 }
202 case "error":
203 t.wantError, err = strconv.ParseBool(value)
204 if err != nil {
205 return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
206 }
207 case "proxyVersions":
208 if len(value) == 0 {
209 break
210 }
211 proxyVersions := make(map[module.Version]bool)
212 parts := strings.Split(value, ",")
213 for _, modpathWithVersion := range parts {
214 vParts := strings.Split(modpathWithVersion, "@")
215 if len(vParts) != 2 {
216 return nil, fmt.Errorf("proxyVersions entry %s is invalid: it should be of the format <modpath>@v<semver> (ex: github.com/foo/bar@v1.2.3)", modpathWithVersion)
217 }
218 modPath, version := vParts[0], vParts[1]
219 mv := module.Version{
220 Path: modPath,
221 Version: version,
222 }
223 proxyVersions[mv] = true
224 }
225 t.proxyVersions = proxyVersions
226 case "vcs":
227 t.vcs = value
228 default:
229 return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key)
230 }
231 }
232 if t.modPath == "" && (t.version != "" || (t.baseVersion != "" && t.baseVersion != "none")) {
233 return nil, fmt.Errorf("%s: version or base was set but mod was not set", testPath)
234 }
235
236 haveFiles := false
237 for _, f := range t.Files {
238 if f.Name == "want" {
239 t.want = bytes.TrimSpace(f.Data)
240 continue
241 }
242 haveFiles = true
243 }
244
245 if haveFiles && t.version != "" {
246 return nil, fmt.Errorf("%s: version is set but files are present", testPath)
247 }
248
249 return t, nil
250 }
251
252
253
254 func updateTest(t *test, want []byte) error {
255 var wantFile *txtar.File
256 for i := range t.Files {
257 if t.Files[i].Name == "want" {
258 wantFile = &t.Files[i]
259 break
260 }
261 }
262 if wantFile == nil {
263 t.Files = append(t.Files, txtar.File{Name: "want"})
264 wantFile = &t.Files[len(t.Files)-1]
265 }
266
267 wantFile.Data = want
268 data := txtar.Format(&t.Archive)
269 return os.WriteFile(t.testPath, data, 0666)
270 }
271
272 func TestRelease(t *testing.T) {
273 testPaths, err := filepath.Glob(filepath.FromSlash("testdata/*/*.test"))
274 if err != nil {
275 t.Fatal(err)
276 }
277 if len(testPaths) == 0 {
278 t.Fatal("no .test files found in testdata directory")
279 }
280
281 var tests []*test
282 for _, testPath := range testPaths {
283 test, err := readTest(testPath)
284 if err != nil {
285 t.Fatal(err)
286 }
287 tests = append(tests, test)
288 }
289
290 defaultContext, cleanup, err := prepareProxy(nil, tests)
291 if err != nil {
292 t.Fatalf("preparing test proxy: %v", err)
293 }
294 t.Cleanup(cleanup)
295
296 for _, test := range tests {
297 testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(test.testPath), "testdata/"), ".test")
298 t.Run(testName, testRelease(defaultContext, tests, test))
299 }
300 }
301
302 func TestRelease_gitRepo_uncommittedChanges(t *testing.T) {
303 ctx := context.Background()
304 buf := &bytes.Buffer{}
305 releaseDir, err := os.MkdirTemp("", "")
306 if err != nil {
307 t.Fatal(err)
308 }
309
310 goModInit(t, releaseDir)
311 gitInit(t, releaseDir)
312
313
314 bContents := `package b
315 const B = "b"`
316 if err := os.WriteFile(filepath.Join(releaseDir, "b.go"), []byte(bContents), 0644); err != nil {
317 t.Fatal(err)
318 }
319
320 success, err := runRelease(ctx, buf, releaseDir, nil)
321 if got, want := err.Error(), fmt.Sprintf("repo %s has uncommitted changes", releaseDir); got != want {
322 t.Errorf("runRelease:\ngot error:\n%q\nwant error\n%q", got, want)
323 }
324 if success {
325 t.Errorf("runRelease: expected failure, got success")
326 }
327 }
328
329 func testRelease(ctx context.Context, tests []*test, test *test) func(t *testing.T) {
330 return func(t *testing.T) {
331 if test.skip != "" {
332 t.Skip(test.skip)
333 }
334
335 t.Parallel()
336
337 if len(test.proxyVersions) > 0 {
338 var cleanup func()
339 var err error
340 ctx, cleanup, err = prepareProxy(test.proxyVersions, tests)
341 if err != nil {
342 t.Fatalf("preparing test proxy: %v", err)
343 }
344 t.Cleanup(cleanup)
345 }
346
347
348
349 testDir, err := os.MkdirTemp("", "")
350 if err != nil {
351 t.Fatal(err)
352 }
353 if *testwork {
354 fmt.Fprintf(os.Stderr, "test dir: %s\n", testDir)
355 } else {
356 t.Cleanup(func() {
357 os.RemoveAll(testDir)
358 })
359 }
360
361 var arc *txtar.Archive
362 if test.version != "" {
363 arcBase := fmt.Sprintf("%s_%s.txt", strings.ReplaceAll(test.modPath, "/", "_"), test.version)
364 arcPath := filepath.Join("testdata/mod", arcBase)
365 var err error
366 arc, err = txtar.ParseFile(arcPath)
367 if err != nil {
368 t.Fatal(err)
369 }
370 } else {
371 arc = &test.Archive
372 }
373 if err := extractTxtar(testDir, arc); err != nil {
374 t.Fatal(err)
375 }
376
377 switch test.vcs {
378 case "git":
379
380
381 gitInit(t, testDir)
382 case "hg":
383
384
385 hgInit(t, testDir)
386 case "":
387
388 default:
389 t.Fatalf("unknown vcs %q", test.vcs)
390 }
391
392
393 var args []string
394 if test.baseVersion != "" {
395 args = append(args, "-base="+test.baseVersion)
396 }
397 if test.releaseVersion != "" {
398 args = append(args, "-version="+test.releaseVersion)
399 }
400 buf := &bytes.Buffer{}
401 releaseDir := filepath.Join(testDir, test.dir)
402 success, err := runRelease(ctx, buf, releaseDir, args)
403 if err != nil {
404 if !test.wantError {
405 t.Fatalf("unexpected error: %v", err)
406 }
407 if errMsg := []byte(err.Error()); !bytes.Equal(errMsg, bytes.TrimSpace(test.want)) {
408 if *updateGolden {
409 if err := updateTest(test, errMsg); err != nil {
410 t.Fatal(err)
411 }
412 } else {
413 t.Fatalf("got error: %s; want error: %s", errMsg, test.want)
414 }
415 }
416 return
417 }
418 if test.wantError {
419 t.Fatalf("got success; want error %s", test.want)
420 }
421
422 got := bytes.TrimSpace(buf.Bytes())
423 if filepath.Separator != '/' {
424 got = bytes.ReplaceAll(got, []byte{filepath.Separator}, []byte{'/'})
425 }
426 if !bytes.Equal(got, test.want) {
427 if *updateGolden {
428 if err := updateTest(test, got); err != nil {
429 t.Fatal(err)
430 }
431 } else {
432 t.Fatalf("got:\n%s\n\nwant:\n%s", got, test.want)
433 }
434 }
435 if success != test.wantSuccess {
436 t.Fatalf("got success: %v; want success %v", success, test.wantSuccess)
437 }
438 }
439 }
440
441
442 func hgInit(t *testing.T, dir string) {
443 t.Helper()
444
445 if err := os.Mkdir(filepath.Join(dir, ".hg"), 0777); err != nil {
446 t.Fatal(err)
447 }
448
449 if err := os.WriteFile(filepath.Join(dir, ".hg", "branch"), []byte("default"), 0777); err != nil {
450 t.Fatal(err)
451 }
452 }
453
454
455 func gitInit(t *testing.T, dir string) {
456 t.Helper()
457
458 if !hasGit() {
459 t.Skip("PATH does not contain git")
460 }
461
462 stdout := &bytes.Buffer{}
463 stderr := &bytes.Buffer{}
464
465 for _, args := range [][]string{
466 {"git", "init"},
467 {"git", "config", "user.name", "Gopher"},
468 {"git", "config", "user.email", "gopher@golang.org"},
469 {"git", "checkout", "-b", "test"},
470 {"git", "add", "-A"},
471 {"git", "commit", "-m", "test"},
472 } {
473 cmd := exec.Command(args[0], args[1:]...)
474 cmd.Dir = dir
475 cmd.Stdout = stdout
476 cmd.Stderr = stderr
477 if err := cmd.Run(); err != nil {
478 cmdArgs := strings.Join(args, " ")
479 t.Fatalf("%s\n%s\nerror running %q on dir %s: %v", stdout.String(), stderr.String(), cmdArgs, dir, err)
480 }
481 }
482 }
483
484
485 func goModInit(t *testing.T, dir string) {
486 t.Helper()
487
488 aContents := `package a
489 const A = "a"`
490 if err := os.WriteFile(filepath.Join(dir, "a.go"), []byte(aContents), 0644); err != nil {
491 t.Fatal(err)
492 }
493
494 stdout := &bytes.Buffer{}
495 stderr := &bytes.Buffer{}
496 cmd := exec.Command("go", "mod", "init", "example.com/uncommitted")
497 cmd.Stdout = stdout
498 cmd.Stderr = stderr
499 cmd.Dir = dir
500 if err := cmd.Run(); err != nil {
501 t.Fatalf("error running `go mod init`: %s, %v", stderr.String(), err)
502 }
503 }
504
View as plain text