mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 03:11:46 +00:00
test: freeze direct instance-state consumers
Add an AST ratchet over production code that fails when a new file names the daemon's instance-state bucket, record keys, local-file API, the raw vm.InstanceRecord or its conversion, or a JetStreamManager instance-state method. Today's 73 uses form a shrink-only allowlist keyed to the slice 0 inventory; stale entries fail, and a synthetic-source self-test proves the matcher detects each kind of use and ignores declarations.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package instancestate
|
||||
|
||||
// allowance is one recorded production consumer of raw instance state.
|
||||
type allowance struct {
|
||||
File, Symbol string
|
||||
Inv string // inventory item in docs/PACKAGE_BOUNDARY_MIGRATION.md
|
||||
}
|
||||
|
||||
// allowlist mirrors the ADR-0007 slice 0 inventory in
|
||||
// docs/PACKAGE_BOUNDARY_MIGRATION.md. It only shrinks: delete an entry as soon
|
||||
// as its use is gone, and never add one to make a new consumer pass.
|
||||
var allowlist = []allowance{
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.LocalStatePath", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.ReadLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.MarshalLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.WriteLocalStateBytes", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.JetStreamManager", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitTerminatedInstanceBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteNodeMarkerBestEffort", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/instance_record_repair.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-04"},
|
||||
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "daemon.JetStreamManager", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadState", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-05"},
|
||||
|
||||
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/shutdown.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/orphan_qemu_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-06"},
|
||||
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-07"},
|
||||
|
||||
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-08"},
|
||||
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-08"},
|
||||
|
||||
{File: "spinifex/daemon/daemon_mgmt_ip.go", Symbol: "daemon.JetStreamManager", Inv: "INV-09"},
|
||||
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "vm.InstanceRecord", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ClaimRecoverableInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ReleaseRecoveredInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.AbandonRecovery", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-10"},
|
||||
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-11"},
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-11"},
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-11"},
|
||||
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "vm.InstanceRecord", Inv: "INV-12"},
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-12"},
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstanceRecords", Inv: "INV-12"},
|
||||
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.LocalState", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.LocalStatePath", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.ReadLocalState", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-13"},
|
||||
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "vm.InstanceRecord", Inv: "INV-13"},
|
||||
|
||||
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "vm.InstanceRecord", Inv: "INV-14"},
|
||||
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "vm.VMFromRecord", Inv: "INV-14"},
|
||||
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-14"},
|
||||
|
||||
{File: "spinifex/domains/network/reconcile/intent.go", Symbol: `literal "spinifex-instance-state"`, Inv: "INV-15"},
|
||||
{File: "spinifex/domains/network/reconcile/intent.go", Symbol: "vm.InstanceRecord", Inv: "INV-15"},
|
||||
|
||||
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-16"},
|
||||
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-16"},
|
||||
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "vm.InstanceRecord", Inv: "INV-16"},
|
||||
|
||||
{File: "spinifex/runtime/compute/cache/cache.go", Symbol: "vm.InstanceRecord", Inv: "INV-17"},
|
||||
{File: "spinifex/runtime/compute/cache/cache.go", Symbol: "vm.VMFromRecord", Inv: "INV-17"},
|
||||
|
||||
{File: "spinifex/domains/admission/quota/records.go", Symbol: "vm.InstanceRecord", Inv: "INV-18"},
|
||||
|
||||
{File: "spinifex/operator/cli/cluster.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-19"},
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package instancestate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
unrecorded []use
|
||||
stale []allowance
|
||||
malformed []string
|
||||
}
|
||||
|
||||
// check compares the scanned uses with allow.
|
||||
func check(s scan, allow []allowance) report {
|
||||
var rep report
|
||||
allowed := map[use]bool{}
|
||||
for _, a := range allow {
|
||||
u := use{File: a.File, Symbol: a.Symbol}
|
||||
switch {
|
||||
case allowed[u]:
|
||||
rep.malformed = append(rep.malformed, "duplicate entry "+u.String())
|
||||
case a.Inv == "":
|
||||
rep.malformed = append(rep.malformed, "entry "+u.String()+" cites no inventory item")
|
||||
}
|
||||
allowed[u] = true
|
||||
}
|
||||
for _, u := range s.sorted() {
|
||||
if !allowed[u] {
|
||||
rep.unrecorded = append(rep.unrecorded, u)
|
||||
}
|
||||
}
|
||||
for _, a := range allow {
|
||||
if _, ok := s[use{File: a.File, Symbol: a.Symbol}]; !ok {
|
||||
rep.stale = append(rep.stale, a)
|
||||
}
|
||||
}
|
||||
return rep
|
||||
}
|
||||
|
||||
// TestADR0007_S6_NoNewRawInstanceStateConsumers is the blocking ratchet: no
|
||||
// production file may start naming the daemon's instance-state bucket, keys,
|
||||
// local file, raw record or JetStreamManager instance methods.
|
||||
func TestADR0007_S6_NoNewRawInstanceStateConsumers(t *testing.T) {
|
||||
root, err := moduleRoot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, files, err := scanModule(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Guards against a walk that silently finds nothing and passes vacuously.
|
||||
if files < 500 || len(s) < 20 {
|
||||
t.Fatalf("scanned %d files and found %d uses under %s; the walk has lost the tree", files, len(s), root)
|
||||
}
|
||||
if len(allowlist) == 0 {
|
||||
t.Fatal("the allowlist is empty, so the stale check would pass vacuously")
|
||||
}
|
||||
rep := check(s, allowlist)
|
||||
|
||||
t.Run("NoUnrecordedConsumer", func(t *testing.T) {
|
||||
for _, u := range rep.unrecorded {
|
||||
t.Errorf("ADR-0007 S6 (consumers receive narrow state capabilities rather than raw stores): %s at %s; "+
|
||||
"consume an instance-domain capability instead of the daemon's raw instance state", u, s[u])
|
||||
}
|
||||
})
|
||||
t.Run("Allowlist_NoStaleEntries", func(t *testing.T) {
|
||||
for _, a := range rep.stale {
|
||||
t.Errorf("allowlist entry %s uses %s (%s) no longer matches; delete it and update the inventory",
|
||||
a.File, a.Symbol, a.Inv)
|
||||
}
|
||||
})
|
||||
t.Run("Allowlist_WellFormed", func(t *testing.T) {
|
||||
for _, m := range rep.malformed {
|
||||
t.Errorf("allowlist_test.go: %s", m)
|
||||
}
|
||||
t.Logf("%d allow-listed uses", len(allowlist))
|
||||
})
|
||||
t.Run("OwnerFilesExist", func(t *testing.T) {
|
||||
for _, f := range ownerFiles {
|
||||
if _, err := os.Stat(filepath.Join(root, f)); err != nil {
|
||||
t.Errorf("owner file %s is gone; remove it from ownerFiles and re-scan its consumers: %v", f, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const faultSource = `package %s
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
spxd "github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
)
|
||||
|
||||
type holder struct {
|
||||
LoadInstanceRecord func() // a field name, not a use
|
||||
m *spxd.JetStreamManager
|
||||
}
|
||||
|
||||
func (h holder) WriteRunningSet() {} // a declaration, not a use
|
||||
|
||||
func use(h holder, raw []byte, jsManager *spxd.JetStreamManager, other interface{ LoadState() }) {
|
||||
_ = spxd.InstanceStateBucket
|
||||
_ = spxd.LocalStatePath("")
|
||||
var r vm.InstanceRecord
|
||||
_ = json.Unmarshal(raw, &r)
|
||||
_ = vm.VMFromRecord(&r)
|
||||
_ = (&vm.VM{}).Record()
|
||||
_, _ = h.m.LoadInstanceRecord("i-1")
|
||||
_, _, _ = jsManager.LoadState("node")
|
||||
other.LoadState()
|
||||
_ = "spinifex-instance-state"
|
||||
}
|
||||
`
|
||||
|
||||
func parseFault(t *testing.T, rel, pkg string) scan {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, rel, fmt.Sprintf(faultSource, pkg), parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := scan{}
|
||||
scanFile(fset, rel, f, out)
|
||||
return out
|
||||
}
|
||||
|
||||
// TestADR0007_S6_RatchetDetectsFaults injects a new consumer, a stale entry
|
||||
// and a malformed entry, and checks the matcher's declaration exclusions.
|
||||
func TestADR0007_S6_RatchetDetectsFaults(t *testing.T) {
|
||||
const rel = "spinifex/domains/widget/w.go"
|
||||
s := parseFault(t, rel, "widget")
|
||||
|
||||
want := []string{
|
||||
"daemon.InstanceStateBucket", "daemon.LocalStatePath", "daemon.JetStreamManager",
|
||||
"vm.InstanceRecord", "vm.VMFromRecord", "VM.Record()",
|
||||
"JetStreamManager.LoadInstanceRecord", "JetStreamManager.LoadState",
|
||||
`literal "spinifex-instance-state"`,
|
||||
}
|
||||
for _, sym := range want {
|
||||
if _, ok := s[use{File: rel, Symbol: sym}]; !ok {
|
||||
t.Errorf("injected use of %s was not detected; got %v", sym, s.sorted())
|
||||
}
|
||||
}
|
||||
if len(s) != len(want) {
|
||||
t.Errorf("want exactly %d uses (field names, method declarations and an unrelated LoadState excluded), got %v",
|
||||
len(want), s.sorted())
|
||||
}
|
||||
|
||||
stale := allowance{File: "spinifex/domains/gone/g.go", Symbol: "vm.InstanceRecord", Inv: "INV-00"}
|
||||
recorded := allowance{File: rel, Symbol: "vm.InstanceRecord", Inv: "INV-00"}
|
||||
rep := check(s, []allowance{recorded, stale, {File: rel, Symbol: "vm.VMFromRecord"}, recorded})
|
||||
if len(rep.unrecorded) != len(want)-2 {
|
||||
t.Errorf("want %d unrecorded uses, got %v", len(want)-2, rep.unrecorded)
|
||||
}
|
||||
if len(rep.stale) != 1 || rep.stale[0] != stale {
|
||||
t.Errorf("want stale entry %+v, got %+v", stale, rep.stale)
|
||||
}
|
||||
if len(rep.malformed) != 2 {
|
||||
t.Errorf("want a missing-inventory and a duplicate entry reported, got %v", rep.malformed)
|
||||
}
|
||||
}
|
||||
|
||||
// Inside the owning packages the symbols are bare identifiers, and the scan
|
||||
// must see those too.
|
||||
func TestADR0007_S6_RatchetSeesBareIdentifiersInOwnerPackages(t *testing.T) {
|
||||
src := map[string]string{
|
||||
"spinifex/daemon/new_consumer.go": "package daemon\n\nfunc f() string { return InstanceStateBucket + LocalStatePath(\"\") }\n",
|
||||
"spinifex/runtime/compute/vm/new_reader.go": "package vm\n\nfunc g(r *InstanceRecord) *VM { return VMFromRecord(r) }\n",
|
||||
}
|
||||
want := map[string][]string{
|
||||
"spinifex/daemon/new_consumer.go": {"daemon.InstanceStateBucket", "daemon.LocalStatePath"},
|
||||
"spinifex/runtime/compute/vm/new_reader.go": {"vm.InstanceRecord", "vm.VMFromRecord"},
|
||||
}
|
||||
for rel, code := range src {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, rel, code, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := scan{}
|
||||
scanFile(fset, rel, f, out)
|
||||
for _, sym := range want[rel] {
|
||||
if _, ok := out[use{File: rel, Symbol: sym}]; !ok {
|
||||
t.Errorf("%s: bare use of %s not detected; got %v", rel, sym, out.sorted())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Package instancestate freezes the production consumers of the daemon's raw
|
||||
// instance-state persistence, so ADR-0007 S6 and S7 start from a known set
|
||||
// that can only shrink.
|
||||
package instancestate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build/constraint"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
daemonPath = "github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
vmPath = "github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
daemonDir = "spinifex/daemon"
|
||||
vmDir = "spinifex/runtime/compute/vm"
|
||||
)
|
||||
|
||||
// daemonSymbols are the daemon's instance-state bucket, key and local-file
|
||||
// identifiers. A consumer naming one knows where instance state lives.
|
||||
var daemonSymbols = set(
|
||||
"InstanceStateBucket", "InstanceStateBucketVersion",
|
||||
"TerminatedInstanceBucket", "TerminatedInstanceBucketVersion",
|
||||
"InstanceRecordPrefix", "InstanceStatePrefix", "NodePresencePrefix",
|
||||
"StoppedInstancePrefix", "TerminatedInstancePrefix",
|
||||
"LocalState", "LocalStateSchemaVersion", "LocalStatePath", "ReadLocalState",
|
||||
"MarshalLocalState", "WriteLocalStateBytes", "LocalStateFileName",
|
||||
"LocalStateFileMode", "DefaultLocalStateDir",
|
||||
"JetStreamManager", "NewJetStreamManager",
|
||||
)
|
||||
|
||||
// vmSymbols are the raw persisted record and its conversion.
|
||||
var vmSymbols = set("InstanceRecord", "InstanceSpec", "InstanceStatus", "VMFromRecord")
|
||||
|
||||
// jsmMethods are JetStreamManager's instance-state methods. Matched by name, so
|
||||
// a call through an interface that mirrors them counts as a consumer too.
|
||||
var jsmMethods = set(
|
||||
"WriteInstanceRecord", "LoadInstanceRecord", "UpdateInstanceRecord",
|
||||
"DeleteInstanceRecord", "ListInstanceRecords",
|
||||
"WriteTerminatedInstanceRecord", "LoadTerminatedInstanceRecord",
|
||||
"UpdateTerminatedInstanceRecord", "DeleteTerminatedInstanceRecord",
|
||||
"ListTerminatedInstanceRecords",
|
||||
"WriteNodeMarker", "WriteNodeMarkerBestEffort", "WriteRunningSet",
|
||||
"WriteStoppedInstance", "LoadStoppedInstance", "DeleteStoppedInstance",
|
||||
"ClaimStoppedInstance", "ClaimRecoverableInstance", "ReleaseRecoveredInstance",
|
||||
"AbandonRecovery", "UpdateStoppedInstance", "ListStoppedInstances",
|
||||
"WriteTerminatedInstance", "UpdateTerminatedInstance", "ListTerminatedInstances",
|
||||
"DeleteTerminatedInstance", "LoadTerminatedInstance",
|
||||
"InitKVBucket", "InitTerminatedInstanceBucket",
|
||||
)
|
||||
|
||||
// ambiguousJSMMethods share a name with unrelated methods, so they count only
|
||||
// when called on a field or variable named like a JetStreamManager handle.
|
||||
var ambiguousJSMMethods = set("LoadState", "DeleteState")
|
||||
|
||||
// literals duplicate a bucket or file name instead of naming the owner's symbol.
|
||||
var literals = set(`"spinifex-instance-state"`, `"spinifex-terminated-instances"`, `"instance-state.json"`)
|
||||
|
||||
// ownerFiles define the symbols above. They are the implementation being
|
||||
// migrated, not consumers of it, and are listed so a deleted one is noticed.
|
||||
var ownerFiles = []string{
|
||||
"spinifex/daemon/instance_membership.go",
|
||||
"spinifex/daemon/instance_records.go",
|
||||
"spinifex/daemon/instance_records_migrate.go",
|
||||
"spinifex/daemon/instance_running_set.go",
|
||||
"spinifex/daemon/jetstream.go",
|
||||
"spinifex/daemon/local_state.go",
|
||||
"spinifex/runtime/compute/vm/record.go",
|
||||
}
|
||||
|
||||
func set(names ...string) map[string]bool {
|
||||
m := make(map[string]bool, len(names))
|
||||
for _, n := range names {
|
||||
m[n] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// use is one consumer of one symbol, keyed by file so a new file is a new use.
|
||||
type use struct {
|
||||
File, Symbol string
|
||||
}
|
||||
|
||||
func (u use) String() string { return u.File + " uses " + u.Symbol }
|
||||
|
||||
// scan maps each use to the first file:line it appears at.
|
||||
type scan map[use]string
|
||||
|
||||
func moduleRoot() (string, error) {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return "", errors.New("cannot locate test source")
|
||||
}
|
||||
return filepath.Abs(filepath.Join(filepath.Dir(file), "..", ".."))
|
||||
}
|
||||
|
||||
var excludedTopDirs = map[string]bool{"tests": true, "docs": true, "architecture": true}
|
||||
|
||||
// scanModule parses every non-test production Go file under root.
|
||||
func scanModule(root string) (scan, int, error) {
|
||||
out := scan{}
|
||||
files := 0
|
||||
fset := token.NewFileSet()
|
||||
owners := set(ownerFiles...)
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if d.IsDir() {
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") ||
|
||||
name == "testdata" || name == "vendor" || name == "node_modules" || excludedTopDirs[rel] {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := parser.ParseFile(fset, path, src, parser.ParseComments|parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ignoredByBuildTag(f.Comments, f.Package) {
|
||||
return nil
|
||||
}
|
||||
files++
|
||||
if owners[rel] {
|
||||
return nil
|
||||
}
|
||||
scanFile(fset, rel, f, out)
|
||||
return nil
|
||||
})
|
||||
return out, files, err
|
||||
}
|
||||
|
||||
// scanFile records every use in f, which lives at module-relative path rel.
|
||||
func scanFile(fset *token.FileSet, rel string, f *ast.File, out scan) {
|
||||
dir := filepath.ToSlash(filepath.Dir(rel))
|
||||
daemonName, vmName := importName(f, daemonPath, "daemon"), importName(f, vmPath, "vm")
|
||||
inDaemon, inVM := dir == daemonDir, dir == vmDir
|
||||
seesVM := inDaemon || inVM || daemonName != "" || vmName != ""
|
||||
|
||||
record := func(symbol string, pos token.Pos) {
|
||||
u := use{File: rel, Symbol: symbol}
|
||||
if _, ok := out[u]; !ok {
|
||||
out[u] = fmt.Sprintf("%s:%d", rel, fset.Position(pos).Line)
|
||||
}
|
||||
}
|
||||
|
||||
var visit func(n ast.Node) bool
|
||||
visit = func(n ast.Node) bool {
|
||||
switch n := n.(type) {
|
||||
case *ast.ImportSpec:
|
||||
return false
|
||||
case *ast.BasicLit:
|
||||
if n.Kind == token.STRING && literals[n.Value] {
|
||||
record("literal "+n.Value, n.Pos())
|
||||
}
|
||||
case *ast.CallExpr:
|
||||
if sel, ok := n.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Record" && len(n.Args) == 0 && seesVM {
|
||||
record("VM.Record()", sel.Sel.Pos())
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
if x, ok := n.X.(*ast.Ident); ok {
|
||||
switch {
|
||||
case daemonName != "" && x.Name == daemonName && daemonSymbols[n.Sel.Name]:
|
||||
record("daemon."+n.Sel.Name, n.Sel.Pos())
|
||||
case vmName != "" && x.Name == vmName && vmSymbols[n.Sel.Name]:
|
||||
record("vm."+n.Sel.Name, n.Sel.Pos())
|
||||
}
|
||||
}
|
||||
if jsmMethods[n.Sel.Name] || (ambiguousJSMMethods[n.Sel.Name] && jsmReceivers[receiverName(n.X)]) {
|
||||
record("JetStreamManager."+n.Sel.Name, n.Sel.Pos())
|
||||
}
|
||||
// The selected name is a field or method, never a bare package symbol.
|
||||
ast.Inspect(n.X, visit)
|
||||
return false
|
||||
case *ast.FuncDecl:
|
||||
// A method declared with one of these names is an implementation,
|
||||
// not a use; its body is still scanned.
|
||||
if n.Recv != nil {
|
||||
ast.Inspect(n.Recv, visit)
|
||||
}
|
||||
if n.Body != nil {
|
||||
ast.Inspect(n.Body, visit)
|
||||
}
|
||||
if n.Type != nil {
|
||||
ast.Inspect(n.Type, visit)
|
||||
}
|
||||
return false
|
||||
case *ast.Field:
|
||||
// Interface method and struct field names are declarations.
|
||||
if n.Type != nil {
|
||||
ast.Inspect(n.Type, visit)
|
||||
}
|
||||
if n.Tag != nil {
|
||||
ast.Inspect(n.Tag, visit)
|
||||
}
|
||||
return false
|
||||
case *ast.Ident:
|
||||
if inDaemon && daemonSymbols[n.Name] {
|
||||
record("daemon."+n.Name, n.Pos())
|
||||
}
|
||||
if inVM && vmSymbols[n.Name] {
|
||||
record("vm."+n.Name, n.Pos())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
ast.Inspect(f, visit)
|
||||
}
|
||||
|
||||
var jsmReceivers = set("jsManager", "jsm", "js")
|
||||
|
||||
// receiverName is the last name in x: "js" for both js and a.js.
|
||||
func receiverName(x ast.Expr) string {
|
||||
switch x := x.(type) {
|
||||
case *ast.Ident:
|
||||
return x.Name
|
||||
case *ast.SelectorExpr:
|
||||
return x.Sel.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// importName returns the name f refers to path by, or "" if f does not import it.
|
||||
func importName(f *ast.File, path, def string) string {
|
||||
for _, imp := range f.Imports {
|
||||
p, err := strconv.Unquote(imp.Path.Value)
|
||||
if err != nil || p != path {
|
||||
continue
|
||||
}
|
||||
if imp.Name != nil {
|
||||
if imp.Name.Name == "_" || imp.Name.Name == "." {
|
||||
return ""
|
||||
}
|
||||
return imp.Name.Name
|
||||
}
|
||||
return def
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ignoredByBuildTag reports a `//go:build ignore` file, which is a standalone
|
||||
// program rather than part of its directory's package.
|
||||
func ignoredByBuildTag(groups []*ast.CommentGroup, pkgPos token.Pos) bool {
|
||||
for _, cg := range groups {
|
||||
if cg.Pos() >= pkgPos {
|
||||
break
|
||||
}
|
||||
for _, c := range cg.List {
|
||||
if !constraint.IsGoBuild(c.Text) {
|
||||
continue
|
||||
}
|
||||
expr, err := constraint.Parse(c.Text)
|
||||
if err == nil && expr.String() == "ignore" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s scan) sorted() []use {
|
||||
out := make([]use, 0, len(s))
|
||||
for u := range s {
|
||||
out = append(out, u)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].File != out[j].File {
|
||||
return out[i].File < out[j].File
|
||||
}
|
||||
return out[i].Symbol < out[j].Symbol
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user