blob: 6a72e5f48dfb7912d6541feb652c85992d2a8e94 [file] [edit]
// 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"
"fmt"
"strings"
"testing"
"time"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/testing/sample"
)
func TestUpdateSearchDocumentsImportedByCount(t *testing.T) {
t.Parallel()
ctx := context.Background()
pkgPath := func(m *internal.Module) string { return m.Packages()[0].Path }
// Insert a package into a new module "mod.com"/suffix@version, and return the module.
insertPackageVersion := func(t *testing.T, db *DB, suffix, version string, imports []string) *internal.Module {
t.Helper()
m := sample.Module("mod.com/"+suffix, version, suffix)
// Units[0] is the module itself.
pkg := m.Units[1]
pkg.Imports = nil
for _, imp := range imports {
pkg.Imports = append(pkg.Imports, fmt.Sprintf("mod.com/%s/%[1]s", imp))
}
db.MustInsertModule(t, m)
return m
}
updateImportedByCount := func(db *DB) {
t.Helper()
if _, _, err := db.UpdateSearchDocumentsImportedByCount(ctx, nil); err != nil {
t.Fatal(err)
}
}
t.Run("basic", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
// Test imported_by_count = 0 when only pkgA is added.
mA := insertPackageVersion(t, testDB, "A", "v1.0.0", nil)
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 0, 0)
// Test imported_by_count = 1 for pkgA when pkgB is added.
mB := insertPackageVersion(t, testDB, "B", "v1.0.0", []string{"A"})
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 1, 1)
sdB := validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mB), 0, 0)
wantSearchDocBUpdatedAt := sdB.importedByCountUpdatedAt
// Test imported_by_count = 2 for pkgA, when C is added.
mC := insertPackageVersion(t, testDB, "C", "v1.0.0", []string{"A"})
updateImportedByCount(testDB)
sdA := validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 2, 2)
sdC := validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mC), 0, 0)
// Nothing imports C, but it is updated nonetheless.
if sdC.importedByCountUpdatedAt.IsZero() {
t.Fatal("pkgC imported_by_count_updated_at should be non-zero, but is zero")
}
if sdA.importedByCountUpdatedAt.IsZero() {
t.Fatal("pkgA imported_by_count_updated_at should be non-zero, but is zero")
}
// Test imported_by_count for B has not changed,
// but its update time has.
sdB = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mB), 0, 0)
if sdB.importedByCountUpdatedAt.Equal(wantSearchDocBUpdatedAt) {
t.Fatalf("expected imported_by_count_updated_at for pkgB to have changed; old = %v, new = %v",
wantSearchDocBUpdatedAt, sdB.importedByCountUpdatedAt)
}
// When an older version of A imports D, nothing happens to the counts,
// because imports_unique only records the latest version of each package.
mD := insertPackageVersion(t, testDB, "D", "v1.0.0", nil)
insertPackageVersion(t, testDB, "A", "v0.9.0", []string{"D"})
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 2, 2)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mD), 0, 0)
// When a newer version of A imports D, however, the counts do change.
insertPackageVersion(t, testDB, "A", "v1.1.0", []string{"D"})
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 2, 2)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mD), 1, 1)
})
t.Run("alternative", func(t *testing.T) {
// Test with alternative modules that are removed from search_documents.
testDB, release := acquire(t)
defer release()
insertPackageVersion(t, testDB, "B", "v1.0.0", nil) // mod.com/B with package mod.com/B/B
insertPackageVersion(t, testDB, "C", "v1.0.0", nil) // mod.com/C with package mod.com/C/C
// Insert a package with the canonical module path.
canonicalModule := insertPackageVersion(t, testDB, "A", "v1.0.0", []string{"B"})
// mod.com/A/A imports mod.com/B/B.
// Now we see a package with an alternative path at v1.2.0.
// We add that information to module_version_states.
alternativeModulePath := strings.ToLower(canonicalModule.ModulePath)
alternativeStatus := derrors.ToStatus(derrors.AlternativeModule)
mvs := &ModuleVersionStateForUpdate{
ModulePath: alternativeModulePath,
Version: "v1.2.0",
Timestamp: time.Now(),
Status: alternativeStatus,
GoModPath: canonicalModule.ModulePath,
}
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, mvs.Version, "")
if err := testDB.UpdateModuleVersionState(ctx, mvs); err != nil {
t.Fatal(err)
}
// Now we see an earlier version of that package, without a go.mod file, so we insert it.
// saveModule inserts it into imports_unique, but skips search_documents.
mAlt := sample.Module(alternativeModulePath, "v1.0.0", "A")
mAlt.LatestVersion = mvs.Version
mAlt.Packages()[0].Imports = []string{"mod.com/B/B", "mod.com/C/C"}
testDB.MustInsertModule(t, mAlt)
// Although B is in imports_unique as imported by both mod.com/A and mod.com/a,
// only mod.com/A is in search_documents, so both its imported-by counts are 1.
// C is only imported by mod.com/a, so both its imported-by counts are 0.
updateImportedByCount(testDB)
validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/B/B", 1, 1)
validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/C/C", 0, 0)
})
t.Run("multiple", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
// Two modules with importers.
mA := insertPackageVersion(t, testDB, "A", "v1.0.0", nil)
mB := insertPackageVersion(t, testDB, "B", "v1.0.0", nil)
insertPackageVersion(t, testDB, "C", "v1.0.0", []string{"A"})
insertPackageVersion(t, testDB, "D", "v1.0.0", []string{"A"})
insertPackageVersion(t, testDB, "E", "v1.0.0", []string{"B"})
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 2, 2)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mB), 1, 1)
})
t.Run("module_sharing", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
mA := insertPackageVersion(t, testDB, "A", "v1.0.0", nil)
// Module C has two packages, C1 and C2, both importing A.
mC := sample.Module("mod.com/C", "v1.0.0", "C1", "C2")
for _, u := range mC.Units {
if u.Path != "mod.com/C" {
u.Imports = []string{pkgPath(mA)}
}
}
testDB.MustInsertModule(t, mC)
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, pkgPath(mA), 2, 1)
})
t.Run("same_module", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
// Module A has a root package ("mod.com/A") and two subpackages, pkg1 and pkg2.
// Module AB ("mod.com/AB") has a module path that has "mod.com/A" as a string
// prefix, but is a distinct module.
// pkg1 imports the root package mod.com/A, sibling mod.com/A/pkg2, and mod.com/AB/pkg.
mA := sample.Module("mod.com/A", "v1.0.0", "", "pkg1", "pkg2")
for _, u := range mA.Units {
if u.Path == "mod.com/A/pkg1" {
u.Imports = []string{"mod.com/A", "mod.com/A/pkg2", "mod.com/AB/pkg"}
} else if u.IsPackage() {
u.Imports = nil
}
}
testDB.MustInsertModule(t, mA)
mAB := sample.Module("mod.com/AB", "v1.0.0", "pkg")
mAB.Packages()[0].Imports = nil
testDB.MustInsertModule(t, mAB)
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/A", 0, 1)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/A/pkg2", 0, 1)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/AB/pkg", 1, 1)
})
t.Run("stdlib", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
// Standard library package net/http imports fmt (both in module "std").
// External package mod.com/A/A also imports fmt.
mStd := sample.Module(stdlib.ModulePath, "v1.12.5", "fmt", "net/http")
for _, u := range mStd.Units {
switch u.Path {
case "net/http":
u.Imports = []string{"fmt"}
case "fmt":
u.Imports = nil
}
}
testDB.MustInsertModule(t, mStd)
mA := sample.Module("mod.com/A", "v1.0.0", "A")
mA.Packages()[0].Imports = []string{"fmt"}
testDB.MustInsertModule(t, mA)
updateImportedByCount(testDB)
_ = validateImportedByCountAndGetSearchDocument(t, testDB, "net/http", 0, 0)
// For fmt, net/http is excluded from package count (same module "std"),
// while mod.com/A/A is counted. Both modules ("std" and "mod.com/A") are
// counted in imported_by_module_count.
_ = validateImportedByCountAndGetSearchDocument(t, testDB, "fmt", 1, 2)
})
t.Run("limit", func(t *testing.T) {
testDB, release := acquire(t)
defer release()
m := sample.Module("mod.com/A", "v1.0.0", "p1", "p2", "p3", "p4")
testDB.MustInsertModule(t, m)
np, nm, err := testDB.UpdateSearchDocumentsImportedByCount(ctx, &ImporterCountOptions{
BatchSize: 2,
Limit: 3,
})
if err != nil {
t.Fatal(err)
}
if np != 3 || nm != 3 {
t.Errorf("got (%d, %d) updated, want (3, 3)", np, nm)
}
})
}
func TestUpdateSearchDocumentsImportedByCountWithCounts(t *testing.T) {
t.Parallel()
testDB, release := acquire(t)
defer release()
m := sample.Module("mod.com/A", "v1.0.0", "p1", "p2", "p3")
testDB.MustInsertModule(t, m)
counts := map[string]int{
"mod.com/A/p1": 10,
"mod.com/A/p2": 25,
"mod.com/nonexistent": 99,
}
if err := testDB.UpdateSearchDocumentsImportedByCountWithCounts(t.Context(), counts); err != nil {
t.Fatal(err)
}
validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/A/p1", 10, 0)
validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/A/p2", 25, 0)
validateImportedByCountAndGetSearchDocument(t, testDB, "mod.com/A/p3", 0, 0)
}
func validateImportedByCountAndGetSearchDocument(t *testing.T, db *DB, path string, pcount, mcount int) *searchDocument {
t.Helper()
sd, err := getSearchDocument(t.Context(), db, path)
if err != nil {
t.Fatalf("testDB.getSearchDocument(ctx, %q): %v", path, err)
}
if sd.importedByCount > 0 && sd.importedByCountUpdatedAt.IsZero() {
t.Fatalf("importedByCountUpdatedAt for package %q should not be empty if count > 0", path)
}
if pcount != sd.importedByCount {
t.Fatalf("importedByCount for package %q = %d; want = %d", path, sd.importedByCount, pcount)
}
if sd.importedByModuleCountUpdatedAt.IsZero() {
t.Fatalf("importedByModuleCountUpdatedAt for package %q should not be zero", path)
}
if mcount != sd.importedByModuleCount {
t.Fatalf("importedByModuleCount for package %q = %d; want = %d", path, sd.importedByModuleCount, mcount)
}
counts, err := db.GetImportedByCounts(t.Context(), path, "")
if err != nil {
t.Fatalf("testDB.GetImportedByCounts(ctx, %q): %v", path, err)
}
if counts.Packages != sd.importedByCount || !counts.PackagesUpdatedAt.Equal(sd.importedByCountUpdatedAt) ||
counts.Modules != sd.importedByModuleCount || !counts.ModulesUpdatedAt.Equal(sd.importedByModuleCountUpdatedAt) {
t.Fatalf("GetImportedByCounts(%q) = %+v, does not match search_documents row", path, counts)
}
return sd
}