blob: e6468bd52727149e3a438315f0333623fd21a1ea [file]
// Copyright 2026 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 diagnostics
import (
"os"
"testing"
"time"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/server"
. "golang.org/x/tools/gopls/internal/test/integration"
)
// TestIssue64235 deterministically reproduces the importPackage
// "package name is %q, want %q" bug.Errorf reported by telemetry in
// golang/go#64235 (the dominant pkg.Name()=="" bucket).
//
// Mechanism: the snapshot caches file content, but go list reads disk
// directly. If a closed file's on-disk content diverges from the
// cached content (a missed or delayed file-watcher event), two
// consecutive loads can produce inconsistent metadata: the first
// installs mp_a.Name="" (and type-checks a from cached source,
// caching export data with manifest name "a"); the second, after a is
// restored on disk, gives a fresh importer b an edge to a while a's
// own update is discarded by the existing-metadata filter in
// Snapshot.load. importPackage(a) then reads the cached export data
// and observes the name mismatch.
//
// Within a single load, the guards in load.go (the existing-metadata
// filter and the imported.Name=="" edge drop) prevent this state; it
// requires two loads observing different disk states.
//
// See golang/go#NNNNN for the proposed fix direction (recover from
// snapshot/disk incoherence rather than patching this symptom).
func TestIssue64235(t *testing.T) {
t.Skip("golang/go#64235: deterministic repro of a known coherency bug; unskip when fixed")
const aOriginal = "package a\n\ntype T int\n"
const files = `
-- go.mod --
module mod.com
go 1.21
-- a/a.go --
` + aOriginal + `
-- b/b.go --
package b
var V int
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("b/b.go")
env.AfterChange(NoDiagnostics())
aPath := env.Sandbox.Workdir.AbsPath("a/a.go")
gomodPath := env.Sandbox.Workdir.AbsPath("go.mod")
// --- Phase 1: install mp_a.Name="" with cached a.go intact ---
// Truncate a/a.go on disk WITHOUT notifying gopls (simulating
// a missed file-watcher event), then trigger reinit via go.mod.
// go list sees an empty a.go and returns Name=""; go/packages
// falls back CompiledGoFiles=GoFiles, so a is still
// type-checked, but from the cached "package a" source.
// storePackageResults caches export data with manifest name
// "a" under ph_a.key (which incorporates Name="").
if err := os.WriteFile(aPath, nil, 0644); err != nil {
t.Fatal(err)
}
gomod, _ := os.ReadFile(gomodPath)
if err := os.WriteFile(gomodPath, append(gomod, []byte("\n// touched\n")...), 0644); err != nil {
t.Fatal(err)
}
if err := env.Editor.Server.DidChangeWatchedFiles(env.Ctx, &protocol.DidChangeWatchedFilesParams{
Changes: []protocol.FileEvent{{URI: env.Sandbox.Workdir.URI("go.mod"), Type: protocol.Changed}},
}); err != nil {
t.Fatal(err)
}
env.Await(CompletedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), 1, true))
// storePackageResults runs asynchronously. In practice phase
// 2's own go-list latency provides the necessary wait (0ms
// passed 10/10 in testing), but a small margin avoids flakes
// on slow filesystems.
// TODO(rfindley): replace this sleep with a hook.
time.Sleep(100 * time.Millisecond)
// --- Phase 2: restore a on disk; b adds import "a" ---
// b is invalidated; a is not (b had no a-edge in the prior
// graph, so addRevDeps(b) doesn't reach a). go list for b sees
// the restored a (Name="a") so the b→a edge is kept, but a's
// fresh metadata is discarded by load.go's existing-metadata
// filter. Type-checking b calls importPackage(mp_a{Name=""},
// data{item.Name="a"}) and the bug.Errorf fires.
if err := os.WriteFile(aPath, []byte(aOriginal), 0644); err != nil {
t.Fatal(err)
}
env.SetBufferContent("b/b.go", "package b\n\nimport \"mod.com/a\"\n\nvar V a.T\n")
// The bug.Errorf panic (under PanicOnBugs) is recovered by
// iimportCommon's defer/recover and the resulting error is
// swallowed by getPackage's errgroup, so the only observable
// effect is the spurious "could not import" diagnostic on b.
// In this test that diagnostic can only arise from the
// importPackage failure: a is a valid package and b's import
// is well-formed.
//
// Once golang/go#64235 is fixed there should be no diagnostic
// at all here, so flip this assertion to NoDiagnostics().
env.AfterChange(
Diagnostics(env.AtRegexp("b/b.go", `"mod.com/a"`), WithMessage("could not import mod.com/a")),
)
})
}