Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

123 lines
4.2 KiB
Go

package indexer
import (
"context"
"path/filepath"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/graph/store_sqlite"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
)
// TestShadowSwap_ResolverFollowsGraphPointer guards against the regression
// where the indexer's in-memory shadow swap reassigned idx.graph but left
// idx.resolver pointing at the empty disk Store. The symptom was that
// every resolver pass (module attribution, relative imports, edge in-place
// resolution, ...) silently no-op'd for any backend that opted into the
// shadow swap — because the resolver's r.graph.EdgesWithUnresolvedTarget()
// returned 0 against the empty disk store and ResolveAll short-circuited
// on len(pending) == 0.
//
// The test indexes the same Python project twice — once into an in-memory
// *Graph (no shadow swap), once into a sqlite *Store (shadow swap engaged)
// — and asserts both produce the same node ID set and the same module
// attribution output (KindModule nodes for pypi imports).
func TestShadowSwap_ResolverFollowsGraphPointer(t *testing.T) {
dir := t.TempDir()
// A pyproject.toml so the dep scanner discovers pypi:requests as
// an external dependency, which the resolver then materialises as
// a KindModule node via attributeNonGoModuleImports.
writeFile(t, filepath.Join(dir, "pyproject.toml"), `
[project]
name = "regression"
dependencies = ["requests>=2.0"]
`)
// Source file imports the pypi package and a stdlib module. Both
// flow through the same module-attribution pass.
writeFile(t, filepath.Join(dir, "app.py"), `
import os
import requests
def fetch(url):
return requests.get(url).text
`)
newIdx := func(t *testing.T, g graph.Store) *Indexer {
t.Helper()
reg := parser.NewRegistry()
reg.Register(languages.NewPythonExtractor())
cfg := config.Default().Index
cfg.Workers = 2
return New(g, reg, cfg, zap.NewNop())
}
indexAndCollect := func(t *testing.T, g graph.Store) map[string]string {
t.Helper()
_, err := newIdx(t, g).IndexCtx(context.Background(), dir)
require.NoError(t, err)
ids := map[string]string{}
for _, n := range g.AllNodes() {
ids[n.ID] = string(n.Kind)
}
return ids
}
memG := graph.New()
memIDs := indexAndCollect(t, memG)
sqliteDir := t.TempDir()
sqliteStore, err := store_sqlite.Open(filepath.Join(sqliteDir, "store.sqlite"))
require.NoError(t, err)
t.Cleanup(func() { _ = sqliteStore.Close() })
// Sanity: sqlite implements BulkLoader so the shadow swap engages.
_, isBulk := graph.Store(sqliteStore).(graph.BulkLoader)
require.True(t, isBulk, "sqlite must implement BulkLoader for this regression to exercise the shadow swap")
dskIDs := indexAndCollect(t, sqliteStore)
// The KindModule node the resolver materialises for `import requests`
// is the canary — without the fix it never gets written, because
// ResolveAll short-circuits before attributeNonGoModuleImports runs.
require.Contains(t, memIDs, "module::pypi:requests",
"baseline: in-memory backend must materialise the pypi module node")
assert.Contains(t, dskIDs, "module::pypi:requests",
"shadow-swap path must materialise the pypi module node — regression: resolver pointed at empty disk store")
// Stdlib import gets the same treatment.
require.Contains(t, memIDs, "module::python:stdlib::os",
"baseline: in-memory backend must materialise the python stdlib module node")
assert.Contains(t, dskIDs, "module::python:stdlib::os",
"shadow-swap path must materialise the python stdlib module node")
// Beyond the canary, both backends must produce the same set of
// node IDs. Any divergence means some resolver pass is still missing
// from one of the two paths.
onlyMem := setDiff(memIDs, dskIDs)
onlyDsk := setDiff(dskIDs, memIDs)
sort.Strings(onlyMem)
sort.Strings(onlyDsk)
assert.Empty(t, onlyMem, "nodes only in memory: %v", onlyMem)
assert.Empty(t, onlyDsk, "nodes only in sqlite: %v", onlyDsk)
}
func setDiff(a, b map[string]string) []string {
out := []string{}
for id := range a {
if _, ok := b[id]; !ok {
out = append(out, id)
}
}
return out
}