| // Copyright 2021 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package postgres |
| |
| import ( |
| "context" |
| "database/sql" |
| "errors" |
| "fmt" |
| "testing" |
| |
| "golang.org/x/pkgsite/internal/database" |
| "golang.org/x/pkgsite/internal/testing/sample" |
| ) |
| |
| func TestGetLatestMajorPathForV1Path(t *testing.T) { |
| t.Parallel() |
| ctx := context.Background() |
| |
| for _, test := range []struct { |
| name string |
| v1ModulePath string |
| modvers []string |
| wantModulePath string |
| wantVersion int |
| }{ |
| { |
| "want highest major version", |
| "m.com", |
| []string{"m.com@v1.0.0", "m.com/v2@v2.0.0", "m.com/v11@v11.0.0"}, |
| "m.com/v11", 11, |
| }, |
| { |
| "only v1 version", |
| "m.com", |
| []string{"m.com@v1.0.0"}, |
| "m.com", 1, |
| }, |
| { |
| "no v1 version", |
| "m.com", |
| []string{"m.com/v4@v4.0.0"}, |
| "m.com/v4", 4, |
| }, |
| { |
| "gopkg.in", |
| "gopkg.in/yaml", |
| []string{"gopkg.in/yaml.v1@v1.0.0", "gopkg.in/yaml.v2@v2.0.0"}, |
| "gopkg.in/yaml.v2", 2, |
| }, |
| } { |
| t.Run(test.name, func(t *testing.T) { |
| testDB, release := acquire(t) |
| defer release() |
| |
| const suffix = "a/b/c" |
| |
| check := func(v1path, wantPath string) { |
| t.Helper() |
| gotPath, gotVer, err := testDB.GetLatestMajorPathForV1Path(ctx, v1path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if gotPath != wantPath || gotVer != test.wantVersion { |
| t.Errorf("GetLatestMajorPathForV1Path(%q) = %q, %d, want %q, %d", |
| v1path, gotPath, gotVer, wantPath, test.wantVersion) |
| } |
| } |
| |
| for _, mv := range test.modvers { |
| mod, ver, _ := parseModuleVersionPackage(mv) |
| m := sample.Module(mod, ver, suffix) |
| MustInsertModuleLatest(ctx, t, testDB, m) |
| } |
| t.Run("module", func(t *testing.T) { |
| check(test.v1ModulePath, test.wantModulePath) |
| }) |
| t.Run("package", func(t *testing.T) { |
| check(test.v1ModulePath+"/"+suffix, test.wantModulePath+"/"+suffix) |
| }) |
| }) |
| } |
| } |
| |
| func TestModulePathMajorVersion(t *testing.T) { |
| for _, test := range []struct { |
| in string |
| want int |
| }{ |
| {"m.com", 1}, |
| {"m.com/v123", 123}, |
| {"gopkg.in/m.v1", 1}, |
| {"gopkg.in/m.v35", 35}, |
| } { |
| got, err := modulePathMajorVersion(test.in) |
| if err != nil { |
| t.Fatalf("%s: %v", test.in, err) |
| } |
| if got != test.want { |
| t.Errorf("%s: got %d, want %d", test.in, got, test.want) |
| } |
| } |
| } |
| |
| func TestUpsertPathConcurrently(t *testing.T) { |
| // Verify that we get no constraint violations or other errors when |
| // the same path is upserted multiple times concurrently. |
| t.Parallel() |
| testDB, release := acquire(t) |
| defer release() |
| ctx := context.Background() |
| |
| const n = 10 |
| errc := make(chan error, n) |
| for i := 0; i < n; i++ { |
| go func() { |
| errc <- testDB.db.Transact(ctx, sql.LevelRepeatableRead, func(tx *database.DB) error { |
| id, err := upsertPath(ctx, tx, "a/path") |
| if err != nil { |
| return err |
| } |
| if id == 0 { |
| return errors.New("zero id") |
| } |
| return nil |
| }) |
| }() |
| |
| } |
| for i := 0; i < n; i++ { |
| if err := <-errc; err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| func TestUpsertPaths(t *testing.T) { |
| t.Parallel() |
| testDB, release := acquire(t) |
| defer release() |
| ctx := context.Background() |
| |
| check := func(paths []string) { |
| got, err := upsertPathsInTx(ctx, testDB.db, paths) |
| if err != nil { |
| t.Fatal(err) |
| } |
| checkPathMap(t, got, paths) |
| } |
| |
| check([]string{"a", "b", "c"}) |
| check([]string{"b", "c", "d", "e"}) |
| } |
| |
| func checkPathMap(t *testing.T, got map[string]int, paths []string) { |
| t.Helper() |
| if g, w := len(got), len(paths); g != w { |
| t.Errorf("got %d paths, want %d", g, w) |
| return |
| } |
| for _, p := range paths { |
| g, ok := got[p] |
| if !ok { |
| t.Errorf("missing path %q", p) |
| } else if g == 0 { |
| t.Errorf("path %q has a 0 ID", p) |
| } |
| } |
| } |
| |
| func TestUpsertPathsConcurrently(t *testing.T) { |
| // Verify that we get no constraint violations or other errors when |
| // the same set of paths is upserted multiple times concurrently. |
| t.Parallel() |
| testDB, release := acquire(t) |
| defer release() |
| ctx := context.Background() |
| |
| const n = 10 |
| paths := make([]string, 100) |
| for i := 0; i < len(paths); i++ { |
| paths[i] = fmt.Sprintf("p%d", i) |
| } |
| errc := make(chan error, n) |
| for i := 0; i < n; i++ { |
| i := i |
| go func() { |
| start := (10 * i) % len(paths) |
| end := start + 50 |
| if end > len(paths) { |
| end = len(paths) |
| } |
| sub := paths[start:end] |
| got, err := upsertPathsInTx(ctx, testDB.db, sub) |
| if err == nil { |
| checkPathMap(t, got, sub) |
| |
| } |
| errc <- err |
| }() |
| |
| } |
| for i := 0; i < n; i++ { |
| if err := <-errc; err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| func upsertPathsInTx(ctx context.Context, db *database.DB, paths []string) (map[string]int, error) { |
| var m map[string]int |
| err := db.Transact(ctx, sql.LevelRepeatableRead, func(tx *database.DB) error { |
| var err error |
| m, err = upsertPaths(ctx, tx, paths) |
| return err |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return m, nil |
| } |