| // Copyright 2019 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" | |
| "os" | |
| "path/filepath" | |
| "slices" | |
| "sort" | |
| "strings" | |
| "sync" | |
| "testing" | |
| "time" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/google/go-cmp/cmp/cmpopts" | |
| "github.com/google/licensecheck" | |
| "github.com/google/safehtml" | |
| "golang.org/x/pkgsite/internal" | |
| "golang.org/x/pkgsite/internal/database" | |
| "golang.org/x/pkgsite/internal/derrors" | |
| "golang.org/x/pkgsite/internal/licenses" | |
| "golang.org/x/pkgsite/internal/source" | |
| "golang.org/x/pkgsite/internal/stdlib" | |
| "golang.org/x/pkgsite/internal/testing/sample" | |
| ) | |
| func TestInsertModule(t *testing.T) { | |
| t.Parallel() | |
| ctx := context.Background() | |
| for _, test := range []struct { | |
| name string | |
| module *internal.Module | |
| goMod string | |
| }{ | |
| { | |
| name: "valid test", | |
| module: sample.DefaultModule(), | |
| }, | |
| { | |
| name: "valid test with internal package", | |
| module: sample.Module(sample.ModulePath, sample.VersionString, "internal/foo"), | |
| }, | |
| { | |
| name: "valid test for prerelease version", | |
| module: sample.Module(sample.ModulePath, "v1.0.0-beta", "internal/foo"), | |
| }, | |
| { | |
| name: "valid test for pseudoversion version", | |
| module: sample.Module(sample.ModulePath, "v0.0.0-20210212193344-7015347762c1", "internal/foo"), | |
| }, | |
| { | |
| name: "v2 version", | |
| module: sample.Module(sample.ModulePath+"/v2", "v2.0.0", "foo"), | |
| }, | |
| { | |
| name: "valid test with go.mod missing", | |
| module: func() *internal.Module { | |
| m := sample.DefaultModule() | |
| m.HasGoMod = false | |
| return m | |
| }(), | |
| }, | |
| { | |
| name: "stdlib", | |
| module: sample.Module(stdlib.ModulePath, "v1.12.5", "context"), | |
| }, | |
| { | |
| name: "deprecated", | |
| module: func() *internal.Module { | |
| m := sample.DefaultModule() | |
| m.Deprecated = true | |
| m.DeprecationComment = "use v2" | |
| return m | |
| }(), | |
| goMod: "module " + sample.ModulePath + " // Deprecated: use v2", | |
| }, | |
| { | |
| name: "zero commit time", | |
| module: func() *internal.Module { | |
| v := sample.DefaultModule() | |
| v.CommitTime = time.Time{} | |
| return v | |
| }(), | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| testDB, release := acquire(t) | |
| defer release() | |
| testDB.MustInsertModuleGoMod(ctx, t, test.module, test.goMod) | |
| // Test that insertion of duplicate primary key won't fail. | |
| testDB.MustInsertModuleGoMod(ctx, t, test.module, test.goMod) | |
| checkModule(ctx, t, testDB, test.module) | |
| }) | |
| } | |
| } | |
| func checkModule(ctx context.Context, t *testing.T, db *DB, want *internal.Module) { | |
| got, err := db.GetModuleInfo(ctx, want.ModulePath, want.Version) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if diff := cmp.Diff(want.ModuleInfo, *got, | |
| cmp.AllowUnexported(source.Info{}), | |
| ); diff != "" { | |
| t.Fatalf("testDB.GetModuleInfo(%q, %q) mismatch (-want +got):\n%s", want.ModulePath, want.Version, diff) | |
| } | |
| for _, wantu := range want.Units { | |
| got, err := db.GetUnit(ctx, &wantu.UnitMeta, internal.AllFields, internal.BuildContext{}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| var subdirectories []*internal.PackageMeta | |
| for _, u := range want.Units { | |
| if u.IsPackage() && (strings.HasPrefix(u.Path, wantu.Path) || wantu.Path == stdlib.ModulePath) { | |
| subdirectories = append(subdirectories, packageMetaFromUnit(u)) | |
| } | |
| } | |
| wantu.Subdirectories = subdirectories | |
| opts := cmp.Options{ | |
| cmpopts.EquateEmpty(), | |
| cmpopts.IgnoreFields(licenses.Metadata{}, "Coverage"), | |
| cmp.AllowUnexported(source.Info{}, safehtml.HTML{}), | |
| } | |
| if diff := cmp.Diff(wantu, got, opts); diff != "" { | |
| t.Errorf("mismatch (-want +got):\n%s", diff) | |
| } | |
| } | |
| } | |
| func packageMetaFromUnit(u *internal.Unit) *internal.PackageMeta { | |
| return &internal.PackageMeta{ | |
| Path: u.Path, | |
| IsRedistributable: u.IsRedistributable, | |
| Name: u.Name, | |
| Synopsis: u.Documentation[0].Synopsis, | |
| Licenses: u.Licenses, | |
| } | |
| } | |
| func TestInsertModuleLicenseCheck(t *testing.T) { | |
| t.Parallel() | |
| ctx := context.Background() | |
| for _, bypass := range []bool{false, true} { | |
| t.Run(fmt.Sprintf("bypass=%t", bypass), func(t *testing.T) { | |
| testDB, release := acquire(t) | |
| defer release() | |
| var db *DB | |
| if bypass { | |
| db = NewBypassingLicenseCheck(testDB.db) | |
| } else { | |
| db = testDB | |
| } | |
| checkHasRedistData := func(readme string, doc []byte, want bool) { | |
| t.Helper() | |
| if got := readme != ""; got != want { | |
| t.Errorf("readme: got %t, want %t", got, want) | |
| } | |
| if got := doc != nil; got != want { | |
| t.Errorf("doc: got %t, want %t", got, want) | |
| } | |
| } | |
| mod := sample.Module(sample.ModulePath, sample.VersionString, "") | |
| checkHasRedistData(mod.Units[0].Readme.Contents, mod.Units[0].Documentation[0].Source, true) | |
| mod.IsRedistributable = false | |
| mod.Units[0].IsRedistributable = false | |
| db.MustInsertModule(t, mod) | |
| // New model | |
| u, err := db.GetUnit(ctx, newUnitMeta(mod.ModulePath, mod.ModulePath, mod.Version), internal.AllFields, internal.BuildContext{}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| var ( | |
| source []byte | |
| readme string | |
| ) | |
| if u.Readme != nil { | |
| readme = u.Readme.Contents | |
| } | |
| if u.Documentation != nil { | |
| source = u.Documentation[0].Source | |
| } | |
| checkHasRedistData(readme, source, bypass) | |
| }) | |
| } | |
| } | |
| func TestUpsertModule(t *testing.T) { | |
| t.Parallel() | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| m := sample.Module("upsert.org", "v1.2.3", "dir/p") | |
| // Insert the module. | |
| testDB.MustInsertModule(t, m) | |
| // Change the module, and re-insert. | |
| m.IsRedistributable = !m.IsRedistributable | |
| lic := *m.Licenses[0] | |
| lic.Contents = append(lic.Contents, " and more"...) | |
| sample.ReplaceLicense(m, &lic) | |
| m.Units[0].Readme.Contents += " and more" | |
| testDB.MustInsertModule(t, m) | |
| // The changes should have been saved. | |
| checkModule(ctx, t, testDB, m) | |
| } | |
| func TestInsertModuleErrors(t *testing.T) { | |
| t.Parallel() | |
| ctx := context.Background() | |
| testCases := []struct { | |
| name string | |
| module *internal.Module | |
| // identifiers to use for fetch | |
| wantModulePath, wantVersion, wantPkgPath string | |
| // error conditions | |
| wantWriteErr error | |
| wantReadErr bool | |
| }{ | |
| { | |
| name: "nil version write error", | |
| wantModulePath: sample.ModulePath, | |
| wantVersion: sample.VersionString, | |
| wantWriteErr: derrors.DBModuleInsertInvalid, | |
| }, | |
| { | |
| name: "nonexistent version", | |
| module: sample.DefaultModule(), | |
| wantModulePath: sample.ModulePath, | |
| wantVersion: "v1.2.3", | |
| }, | |
| { | |
| name: "nonexistent module", | |
| module: sample.DefaultModule(), | |
| wantModulePath: "nonexistent_module_path", | |
| wantVersion: "v1.0.0", | |
| wantPkgPath: sample.PackagePath, | |
| }, | |
| { | |
| name: "missing module path", | |
| module: sample.Module("", sample.VersionString), | |
| wantVersion: sample.VersionString, | |
| wantModulePath: sample.ModulePath, | |
| wantWriteErr: derrors.DBModuleInsertInvalid, | |
| }, | |
| { | |
| name: "missing version", | |
| module: func() *internal.Module { | |
| m := sample.DefaultModule() | |
| m.Version = "" | |
| return m | |
| }(), | |
| wantVersion: sample.VersionString, | |
| wantModulePath: sample.ModulePath, | |
| wantWriteErr: derrors.DBModuleInsertInvalid, | |
| }, | |
| } | |
| for _, test := range testCases { | |
| t.Run(test.name, func(t *testing.T) { | |
| testDB, release := acquire(t) | |
| defer release() | |
| if _, err := testDB.InsertModule(ctx, test.module, nil); !errors.Is(err, test.wantWriteErr) { | |
| t.Errorf("error: %v, want write error: %v", err, test.wantWriteErr) | |
| } | |
| }) | |
| } | |
| } | |
| func TestInsertModuleNewCoverage(t *testing.T) { | |
| t.Parallel() | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| m := sample.DefaultModule() | |
| newCoverage := licensecheck.Coverage{ | |
| Percent: 100, | |
| Match: []licensecheck.Match{{ID: "New", Start: 1, End: 10}}, | |
| } | |
| m.Licenses = []*licenses.License{ | |
| { | |
| Metadata: &licenses.Metadata{ | |
| Types: []string{sample.LicenseType}, | |
| FilePath: sample.LicenseFilePath, | |
| Coverage: newCoverage, | |
| }, | |
| Contents: []byte(`Lorem Ipsum`), | |
| }, | |
| } | |
| testDB.MustInsertModule(t, m) | |
| u, err := testDB.GetUnit(ctx, newUnitMeta(m.ModulePath, m.ModulePath, m.Version), internal.AllFields, internal.BuildContext{}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| got := u.LicenseContents[0].Metadata | |
| want := &licenses.Metadata{ | |
| Types: []string{"MIT"}, | |
| FilePath: sample.LicenseFilePath, | |
| Coverage: newCoverage, | |
| } | |
| if !cmp.Equal(got, want) { | |
| t.Errorf("\ngot %+v\nwant %+v", got, want) | |
| } | |
| } | |
| func TestPostgres_ReadAndWriteModuleOtherColumns(t *testing.T) { | |
| t.Parallel() | |
| // Verify that InsertModule correctly populates the columns in the versions | |
| // table that are not in the ModuleInfo struct. | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| type other struct { | |
| sortVersion, seriesPath string | |
| } | |
| v := sample.Module("github.com/user/repo/path/v2", "v1.2.3-beta.4.a", sample.Suffix) | |
| want := other{ | |
| sortVersion: "1,2,3,~beta,4,~a", | |
| seriesPath: "github.com/user/repo/path", | |
| } | |
| testDB.MustInsertModule(t, v) | |
| query := ` | |
| SELECT | |
| sort_version, series_path | |
| FROM | |
| modules | |
| WHERE | |
| module_path = $1 AND version = $2` | |
| row := testDB.db.QueryRow(ctx, query, v.ModulePath, v.Version) | |
| var got other | |
| if err := row.Scan(&got.sortVersion, &got.seriesPath); err != nil { | |
| t.Fatal(err) | |
| } | |
| if got != want { | |
| t.Errorf("\ngot %+v\nwant %+v", got, want) | |
| } | |
| } | |
| func TestInsertModuleLatest(t *testing.T) { | |
| // Check the first return value of InsertModule, which is whether the | |
| // inserted module is the latest good version. Also check that | |
| // latest_module_versions.good_version is populated correctly. | |
| t.Parallel() | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| // These tests are cumulative: actions of earlier tests may affect later ones. | |
| for _, test := range []struct { | |
| version string | |
| cooked string // The latest cooked version, affects incompatible versions; empty => nil lmv | |
| retract string // body of go.mod retract statement | |
| wantIsLatest bool | |
| wantLatestGood string | |
| }{ | |
| { | |
| version: "v2.0.0+incompatible", | |
| cooked: "v2.0.0+incompatible", | |
| wantIsLatest: true, // The only version, so the latest. | |
| }, | |
| { | |
| version: "v1.5.2", | |
| cooked: "v1.9.0", | |
| // Compatible version is later than incompatible. | |
| wantIsLatest: true, | |
| }, | |
| { | |
| version: "v1.5.1", | |
| cooked: "v1.9.0", | |
| // An earlier version; not the latest. | |
| wantIsLatest: false, | |
| wantLatestGood: "v1.5.2", | |
| }, | |
| { | |
| version: "v1.4.0", | |
| cooked: "v1.9.1", // ignore incompatible | |
| retract: "[v1.5.0, v1.6.0]", // all versions except this retracted | |
| wantIsLatest: true, | |
| }, | |
| { | |
| version: "v1.4.1", | |
| cooked: "v1.9.2", // ignore incompatible | |
| retract: "[v1.4.0, v1.6.0]", // all versions retracted, even this one | |
| wantIsLatest: false, | |
| wantLatestGood: "", | |
| }, | |
| } { | |
| t.Run(test.version, func(t *testing.T) { | |
| m := sample.DefaultModule() | |
| m.Version = test.version | |
| var lmv *internal.LatestModuleVersions | |
| if test.cooked != "" { | |
| gomod := "module " + m.ModulePath | |
| if test.retract != "" { | |
| gomod += "\nretract " + test.retract | |
| } | |
| lmv = addLatest(ctx, t, testDB, m.ModulePath, test.cooked, gomod) | |
| } | |
| gotIsLatest, err := testDB.InsertModule(ctx, m, lmv) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if gotIsLatest != test.wantIsLatest { | |
| t.Errorf("got latest %t, want %t", gotIsLatest, test.wantIsLatest) | |
| } | |
| gotLMV, err := testDB.GetLatestModuleVersions(ctx, m.ModulePath) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if gotLMV == nil { | |
| t.Fatal("gotLMV is nil") | |
| } | |
| wantLatestGood := test.wantLatestGood | |
| if wantLatestGood == "" && test.wantIsLatest { | |
| wantLatestGood = test.version | |
| } | |
| if gotLMV.GoodVersion != wantLatestGood { | |
| t.Errorf("got good version %q, want %q", gotLMV.GoodVersion, wantLatestGood) | |
| } | |
| }) | |
| } | |
| } | |
| func TestPostgres_NewerAlternative(t *testing.T) { | |
| t.Parallel() | |
| // Verify that packages are not added to search_documents if the module has a newer | |
| // alternative version. | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| const ( | |
| altVersion = "v1.2.0" | |
| okVersion = "v1.0.0" | |
| ) | |
| mvs := &ModuleVersionStateForUpdate{ | |
| ModulePath: "example.com/Mod", | |
| Version: altVersion, | |
| AppVersion: "appVersion", | |
| Timestamp: time.Now(), | |
| Status: derrors.ToStatus(derrors.AlternativeModule), | |
| GoModPath: "example.com/mod", | |
| FetchErr: derrors.AlternativeModule, | |
| } | |
| if err := testDB.InsertIndexVersions(ctx, []*internal.IndexVersion{ | |
| { | |
| Path: mvs.ModulePath, | |
| Version: mvs.Version, | |
| Timestamp: mvs.Timestamp, | |
| }, | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| addLatest(ctx, t, testDB, mvs.ModulePath, altVersion, "") | |
| if err := testDB.UpdateModuleVersionState(ctx, mvs); err != nil { | |
| t.Fatal(err) | |
| } | |
| m := sample.Module(mvs.ModulePath, okVersion, "p") | |
| testDB.MustInsertModule(t, m) | |
| if _, _, found := GetFromSearchDocuments(ctx, t, testDB, m.Packages()[0].Path); found { | |
| t.Fatal("found package after inserting") | |
| } | |
| } | |
| func TestMakeValidUnicode(t *testing.T) { | |
| t.Parallel() | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| db := testDB.Underlying() | |
| if _, err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS valid_unicode (contents text)`); err != nil { | |
| t.Fatal(err) | |
| } | |
| defer db.Exec(ctx, `DROP TABLE valid_unicode`) | |
| insert := func(s string) error { | |
| _, err := db.Exec(ctx, `INSERT INTO valid_unicode VALUES($1)`, s) | |
| return err | |
| } | |
| check := func(filename string, okRaw bool) { | |
| data, err := os.ReadFile(filepath.Join("testdata", filename)) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| raw := string(data) | |
| err = insert(raw) | |
| if (err == nil) != okRaw { | |
| t.Errorf("%s, raw: got %v, want error: %t", filename, err, okRaw) | |
| } | |
| if err := insert(makeValidUnicode(string(data))); err != nil { | |
| t.Errorf("%s, after making valid: %v", filename, err) | |
| } | |
| } | |
| check("final-nulls", false) | |
| check("gin-gonic", true) | |
| check("subchord", true) | |
| } | |
| func TestLock(t *testing.T) { | |
| t.Parallel() | |
| // Verify that two transactions cannot both hold the same lock, but that every one | |
| // that wants the lock eventually gets it. | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| db := testDB.Underlying() | |
| const n = 4 | |
| errc := make(chan error) | |
| var ( | |
| mu sync.Mutex | |
| lockHeld bool | |
| count int | |
| ) | |
| for range n { | |
| go func() { | |
| errc <- db.Transact(ctx, sql.LevelDefault, func(tx *database.DB) error { | |
| if err := lock(ctx, tx, sample.ModulePath); err != nil { | |
| return err | |
| } | |
| mu.Lock() | |
| h := lockHeld | |
| lockHeld = true | |
| count++ | |
| mu.Unlock() | |
| if h { | |
| return errors.New("lock already held") | |
| } | |
| time.Sleep(50 * time.Millisecond) | |
| mu.Lock() | |
| lockHeld = false | |
| mu.Unlock() | |
| return nil | |
| }) | |
| }() | |
| } | |
| for range n { | |
| if err := <-errc; err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| if count != n { | |
| t.Errorf("got %d, want %d", count, n) | |
| } | |
| } | |
| func TestIsAlternativeModulePath(t *testing.T) { | |
| t.Parallel() | |
| ctx := context.Background() | |
| testDB, release := acquire(t) | |
| defer release() | |
| const modulePathA = "m.com/a" | |
| altModuleStatus := derrors.ToStatus(derrors.AlternativeModule) | |
| // These tests form a sequence. Each test's state affects the next's. | |
| for _, test := range []struct { | |
| modulePath string | |
| version string | |
| status int | |
| want bool | |
| }{ | |
| {"a.com", "v1.4.0", altModuleStatus, false}, // preseeding a different module should not change result for modulePathA | |
| {modulePathA, "v1.2.0", 200, false}, // no 491s | |
| {modulePathA, "v1.1.0", altModuleStatus, false}, // 491 is earlier | |
| {modulePathA, "v1.3.0-pre", altModuleStatus, false}, // still earlier: release beats pre-release | |
| {modulePathA, "v1.3.0", altModuleStatus, true}, // latest version is 491 | |
| {modulePathA, "v1.4.0", 200, false}, // new latest version is OK | |
| {modulePathA, "v1.5.0", altModuleStatus, true}, // "I can do this all day." --Captain America | |
| } { | |
| mvs := &ModuleVersionStateForUpdate{ | |
| ModulePath: test.modulePath, | |
| Version: test.version, | |
| AppVersion: "appVersion", | |
| Timestamp: time.Now(), | |
| Status: test.status, | |
| HasGoMod: true, | |
| } | |
| if err := testDB.InsertIndexVersions(ctx, []*internal.IndexVersion{ | |
| { | |
| Path: mvs.ModulePath, | |
| Version: mvs.Version, | |
| Timestamp: mvs.Timestamp, | |
| }, | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| addLatest(ctx, t, testDB, test.modulePath, test.version, "") | |
| if err := testDB.UpdateModuleVersionState(ctx, mvs); err != nil { | |
| t.Fatal(err) | |
| } | |
| got, err := isAlternativeModulePath(ctx, testDB.db, modulePathA) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if got != test.want { | |
| t.Fatalf("%s@%s, %d: got %t, want %t", test.modulePath, test.version, test.status, got, test.want) | |
| } | |
| } | |
| } | |
| func TestReconcileSearch(t *testing.T) { | |
| testDB, release := acquire(t) | |
| defer release() | |
| ctx := context.Background() | |
| insert := func(modulePath, version, suffix string, status int, imports []string, modfile string) { | |
| m := sample.Module(modulePath, version, suffix) | |
| pkg := m.Packages()[0] | |
| pkg.Documentation[0].Synopsis = version | |
| pkg.Imports = imports | |
| if status == 200 { | |
| testDB.MustInsertModuleGoMod(ctx, t, m, modfile) | |
| } else { | |
| addLatest(ctx, t, testDB, modulePath, version, modfile) | |
| } | |
| if err := testDB.InsertIndexVersions(ctx, | |
| []*internal.IndexVersion{ | |
| { | |
| Path: modulePath, | |
| Version: version, | |
| Timestamp: time.Now(), | |
| }, | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := testDB.UpdateModuleVersionState(ctx, &ModuleVersionStateForUpdate{ | |
| ModulePath: modulePath, | |
| Version: version, | |
| AppVersion: "app", | |
| Timestamp: time.Now(), | |
| Status: status, | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := testDB.ReconcileSearch(ctx, modulePath, version, status); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| check := func(modPath, wantVersion, suffix string, wantImports []string) { | |
| t.Helper() | |
| pkgPath := modPath | |
| if suffix != "" { | |
| pkgPath += "/" + suffix | |
| } | |
| var gotVersion, gotSynopsis string | |
| err := testDB.db.QueryRow(ctx, ` | |
| SELECT version, synopsis | |
| FROM search_documents | |
| WHERE module_path = $1 | |
| AND package_path = $2 | |
| `, modPath, pkgPath).Scan(&gotVersion, &gotSynopsis) | |
| if errors.Is(err, sql.ErrNoRows) { | |
| gotVersion = "" | |
| gotSynopsis = "" | |
| } else if err != nil { | |
| t.Fatal(err) | |
| } | |
| if gotVersion != wantVersion && gotSynopsis != wantVersion { | |
| t.Fatalf("got version %q, synopsis %q, want %q for both", gotVersion, gotSynopsis, wantVersion) | |
| } | |
| gotImports, err := database.Collect1[string](ctx, testDB.db, ` | |
| SELECT to_path | |
| FROM imports_unique | |
| WHERE from_path = $1 | |
| AND from_module_path = $2 | |
| `, pkgPath, modPath) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| sort.Strings(gotImports) | |
| sort.Strings(wantImports) | |
| if !cmp.Equal(gotImports, wantImports) { | |
| t.Fatalf("got imports %v, want %v", gotImports, wantImports) | |
| } | |
| } | |
| imports1 := []string{"fmt", "log"} | |
| const modPath1 = "m.com/a" | |
| // Insert a good module. It should be in search_documents and imports_unique. | |
| insert(modPath1, "v1.1.0", "pkg", 200, imports1, "") | |
| check(modPath1, "v1.1.0", "pkg", imports1) | |
| // Insert a higher, good version. It should replace the first. | |
| imports2 := []string{"log", "strings"} | |
| insert(modPath1, "v1.2.0", "pkg", 200, imports2, "") | |
| check(modPath1, "v1.2.0", "pkg", imports2) | |
| // Now an even higher, bad version comes along that retracts v1.2.0. | |
| // The search_documents and imports_unique tables should go back to v1.1.0. | |
| insert(modPath1, "v1.3.0", "pkg", 400, nil, "retract v1.2.0") | |
| check(modPath1, "v1.1.0", "pkg", imports1) | |
| // Now a still higher version comes along that retracts everything. The | |
| // module should no longer be in search_documents or imports_unique. | |
| insert(modPath1, "v1.4.0", "pkg", 200, nil, "retract [v1.0.0, v1.4.0]") | |
| check(modPath1, "", "pkg", nil) | |
| // Insert another good module. | |
| const modPath2 = "m.com/b" | |
| insert(modPath2, "v1.1.0", "pkg", 200, imports1, "") | |
| check(modPath2, "v1.1.0", "pkg", imports1) | |
| // A later version makes this an alternative module. | |
| // The module should be removed from search. | |
| insert(modPath2, "v1.2.0", "pkg", 491, imports1, "") | |
| check(modPath2, "", "pkg", nil) | |
| // Insert a module that is its own package. | |
| const modPath3 = "m.com/c/pkg" | |
| insert(modPath3, "v1.0.0", "", 200, imports1, "") | |
| check(modPath3, "v1.0.0", "", imports1) | |
| // Insert a module that is a prefix. It shouldn't change anything. | |
| insert("m.com/c", "v1.0.0", "pkg", 200, imports1, "") | |
| check(modPath3, "v1.0.0", "", imports1) | |
| } | |
| func TestPathsToInsert(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| m *internal.Module | |
| want []string | |
| }{ | |
| { | |
| name: "v1 module", | |
| m: &internal.Module{ | |
| ModuleInfo: internal.ModuleInfo{ModulePath: "a.com/m"}, | |
| Units: []*internal.Unit{ | |
| {UnitMeta: internal.UnitMeta{Path: "a.com/m/u"}}, | |
| }, | |
| }, | |
| want: []string{"a.com/m", "a.com/m/u"}, | |
| }, | |
| { | |
| name: "v2 module", | |
| m: &internal.Module{ | |
| ModuleInfo: internal.ModuleInfo{ModulePath: "a.com/m/v2"}, | |
| Units: []*internal.Unit{ | |
| {UnitMeta: internal.UnitMeta{Path: "a.com/m/v2/u"}}, | |
| }, | |
| }, | |
| want: []string{"a.com/m", "a.com/m/u", "a.com/m/v2", "a.com/m/v2/u"}, | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| got := pathsToInsert(test.m) | |
| slices.Sort(got) | |
| if diff := cmp.Diff(test.want, got); diff != "" { | |
| t.Errorf("mismatch (-want +got):\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestInsertModuleSkipSymbols(t *testing.T) { | |
| t.Parallel() | |
| ctx := context.Background() | |
| checkSymbolsInserted := func(t *testing.T, db *DB, m *internal.Module) bool { | |
| t.Helper() | |
| var count int | |
| err := db.db.QueryRow(ctx, ` | |
| SELECT count(*) FROM documentation_symbols ds | |
| INNER JOIN documentation d ON d.id = ds.documentation_id | |
| INNER JOIN units u ON u.id = d.unit_id | |
| INNER JOIN modules mod ON mod.id = u.module_id | |
| WHERE mod.module_path = $1 AND mod.version = $2`, | |
| m.ModulePath, m.Version).Scan(&count) | |
| if err != nil && err != sql.ErrNoRows { | |
| t.Fatalf("error when checking for symbols: %v", err) | |
| } | |
| return count > 0 | |
| } | |
| for _, test := range []struct { | |
| name string | |
| modulePath string | |
| numSymbols int | |
| wantInserted bool | |
| }{ | |
| { | |
| name: "skipped gardener", | |
| modulePath: "github.com/gardener/gardener", | |
| wantInserted: false, | |
| }, | |
| { | |
| name: "skipped azure-sdk-for-go mixed casing", | |
| modulePath: "github.com/Azure/azure-Sdk-For-Go", | |
| wantInserted: false, | |
| }, | |
| { | |
| name: "skipped prefix match cdktn", | |
| modulePath: "github.com/cdktn-io/cdktn-provider-aws-go/aws/v22", | |
| wantInserted: false, | |
| }, | |
| { | |
| name: "skipped symbol threshold", | |
| modulePath: "example.com/large-module", | |
| numSymbols: maxSymbols + 1, | |
| wantInserted: false, | |
| }, | |
| { | |
| name: "not skipped stdlib", | |
| modulePath: stdlib.ModulePath, | |
| numSymbols: maxSymbols + 1, | |
| wantInserted: true, | |
| }, | |
| { | |
| name: "not skipped golang.org/x", | |
| modulePath: "golang.org/x/arch", | |
| numSymbols: maxSymbols + 1, | |
| wantInserted: true, | |
| }, | |
| { | |
| name: "not skipped", | |
| modulePath: "example.com/test-module", | |
| wantInserted: true, | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| testDB, release := acquire(t) | |
| defer release() | |
| m := sample.Module(test.modulePath, "v1.2.3", "pkg") | |
| m.Units[1].Documentation[0].API = slices.Repeat([]*internal.Symbol{ | |
| { | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "Foo", | |
| Section: internal.SymbolSectionConstants, | |
| Kind: internal.SymbolKindVariable, | |
| }, | |
| }, | |
| }, max(test.numSymbols, 1)) | |
| testDB.MustInsertModule(t, m) | |
| if got := checkSymbolsInserted(t, testDB, m); got != test.wantInserted { | |
| t.Errorf("symbols inserted: got %t; want %t", got, test.wantInserted) | |
| } | |
| }) | |
| } | |
| } |