Files
go-admin/common/middleware/permission_scan_test.go
T
zhangwenjian 0fa015b6d0 perf👌: stop recompiling patterns when scanning the casbin exclusion list
AuthCheckRole walks CasbinExclude for every non-admin request, and used
casbin's util.KeyMatch2 to test each entry. That delegates to
util.RegexMatch, which is regexp.MatchString - it compiles its pattern on
every call - so a 32-entry list cost about 2,566 allocations per request
before the request reached Enforce.

Test the method first, which rules out most entries with a string
compare, and take the path test from go-admin-core, whose KeyMatch2
answers the same thing without recompiling. The scan drops to 52ns and no
allocations.

The loop moves out of AuthCheckRole so the tests exercise the code a
request runs rather than a copy of it, and an allocation budget fails if
the uncached matcher comes back.
2026-08-31 14:01:32 +08:00

80 lines
2.6 KiB
Go

package middleware
import "testing"
// excluded is excludedFromCasbin with the error dropped: these tests are about
// the answer and its cost, and CasbinExclude has no malformed entry to report.
func excluded(t testing.TB, path, method string) bool {
t.Helper()
ok, err := excludedFromCasbin(method, path)
if err != nil {
t.Fatalf("CasbinExclude holds a pattern that will not compile: %s", err)
}
return ok
}
// TestCasbinExcludeScanMatches pins the behaviour the scan has to keep: an
// excluded route is recognised, a protected one is not, and the method has to
// agree.
func TestCasbinExcludeScanMatches(t *testing.T) {
cases := []struct {
path, method string
want bool
}{
{"/api/v1/health", "GET", true},
{"/api/v1/login", "POST", true},
{"/api/v1/roleMenuTreeselect/12", "GET", true},
{"/api/v1/dept", "GET", false},
{"/api/v1/sys-user", "GET", false},
// Same path, wrong method: sys-user is excluded for PUT only.
{"/api/v1/sys-user", "PUT", true},
{"/api/v1/health", "POST", false},
}
for _, c := range cases {
if got := excluded(t, c.path, c.method); got != c.want {
t.Errorf("excludedFromCasbin(%s %s) = %v, want %v", c.method, c.path, got, c.want)
}
}
}
// TestCasbinExcludeScanAllocationBudget is what keeps the scan cheap.
//
// The list is walked per request with a pattern match per entry, and
// casbin's util.KeyMatch2 compiles a regexp on every call - the whole scan
// cost about 2,566 allocations that way. Going back to it fails this test.
//
// Allocation counts are deterministic across machines; wall-clock is not.
func TestCasbinExcludeScanAllocationBudget(t *testing.T) {
// A protected route, so the scan runs to the end without an early match -
// the case every authenticated business request hits.
const path, method = "/api/v1/dept", "GET"
if excluded(t, path, method) {
t.Fatalf("setup failed: %s is in the exclusion list", path)
}
// The budget covers the GET entries that carry a path parameter, which
// still need a match. Measured at 0 for the cached matcher; the headroom
// is for entries being added to the list.
const budget = 64
got := testing.AllocsPerRun(100, func() {
_, _ = excludedFromCasbin(method, path)
})
if got > budget {
t.Errorf("scanning CasbinExclude allocates %.0f times, budget is %d\n"+
"casbin's util.KeyMatch2 costs about 2566 here; use mycasbin.KeyMatch2",
got, budget)
}
}
// BenchmarkCasbinExcludeScan reports what the scan adds to a request.
func BenchmarkCasbinExcludeScan(b *testing.B) {
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = excludedFromCasbin("GET", "/api/v1/dept")
}
})
}