textmachine/backend/internal/pipeline/spendlineorder_test.go

79 lines
3 KiB
Go

package pipeline
import (
"go/ast"
"go/parser"
"go/token"
"testing"
)
// spendlineorder_test.go pins ONE lock order, and it pins it in the only place where breaking it is
// invisible: the source.
//
// The order is stated at both of its ends (emitter.mu, spendLine): the emitter's mutex is taken BEFORE
// the store's write connection and never after one, because the mutex is held across store writes. The
// spend-line closure is the single piece of this package's code that executes while the write connection
// is held, so it is the single place the order can be violated — and the violation does not look like a
// deadlock: the store's op timeout breaks the cycle ten seconds later, and the failure surfaces as
// `context deadline exceeded` in whatever unrelated tests happen to be running (backlog row 387,
// measured at thirteen).
//
// A comment cannot hold that. What holds it is the closure having no `e` in scope — and what tells the
// next person they have just put it back is this test.
func TestTheSpendLineClosureCannotReachTheEmitter(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "events.go", nil, 0)
if err != nil {
t.Fatalf("parse events.go: %v", err)
}
var fn *ast.FuncDecl
for _, d := range file.Decls {
f, ok := d.(*ast.FuncDecl)
if ok && f.Name.Name == "spendLine" && f.Recv != nil {
fn = f
break
}
}
if fn == nil {
t.Fatal("spendLine is not in events.go any more — this pin is looking at the wrong place and is asserting nothing")
}
// The receiver's own name, read from the declaration rather than assumed: renaming it must not turn
// this guard off.
if len(fn.Recv.List) != 1 || len(fn.Recv.List[0].Names) != 1 {
t.Fatal("spendLine's receiver has no name — a nameless receiver cannot be captured, but this pin can no longer tell")
}
recv := fn.Recv.List[0].Names[0].Name
// The closure handed to the store: the FuncLit under the `Line:` key.
var closure *ast.FuncLit
ast.Inspect(fn, func(n ast.Node) bool {
kv, ok := n.(*ast.KeyValueExpr)
if !ok {
return true
}
if key, ok := kv.Key.(*ast.Ident); ok && key.Name == "Line" {
if lit, ok := kv.Value.(*ast.FuncLit); ok {
closure = lit
}
}
return true
})
if closure == nil {
t.Fatal("spendLine no longer hands a function literal to store.SpendLine.Line — the shape this pin reads is gone, so it proves nothing")
}
var used []string
ast.Inspect(closure, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok && id.Name == recv {
used = append(used, fset.Position(id.Pos()).String())
}
return true
})
if len(used) > 0 {
t.Fatalf("the spend-line closure reaches the emitter (%q) at %v.\n"+
"That closure runs INSIDE the store's write transaction, and the emitter's mutex is held across store writes — "+
"so any method taken on it from here closes a lock cycle that the store's timeout breaks ten seconds later, "+
"in whatever tests happen to be running (backlog row 387). Capture what you need by value before the closure instead.",
recv, used)
}
}