Files
xiaomihome/miot/miot_matcher_test.go
T
4566704 35efed7d15 chore: 修复 import 路径为 xiaomihome/miot,补充 README 项目文档
go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为
"xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目
介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构
设计文档。
2026-06-28 23:21:18 +08:00

405 lines
11 KiB
Go

// Package miot_test provides black-box enhancement tests for miot_matcher.go.
// Basic Sub/Match/Unsub are covered in miot_test.go; this file covers
// wildcards, concurrency, edge cases, Clear, and Size.
package miot_test
import (
"sync"
"testing"
"time"
miot "xiaomihome/miot"
)
// ============================================================================
// Wildcard matching: #
// ============================================================================
func TestMatcher_MultiLevelWildcard(t *testing.T) {
matcher := miot.NewMIoTMatcher()
called := make(chan string, 10)
handler := func(params map[string]interface{}, ctx interface{}) {
called <- "handler"
}
// Subscribe with # wildcard (matches everything under did1/p/)
matcher.Sub("did1/p/#", handler, nil)
// Should match deep nested topic
entries := matcher.Match("did1/p/2/1")
if len(entries) != 1 {
t.Errorf("did1/p/2/1 should match did1/p/#, got %d entries", len(entries))
}
// Should match shallow
entries = matcher.Match("did1/p/2")
if len(entries) != 1 {
t.Errorf("did1/p/2 should match did1/p/#, got %d entries", len(entries))
}
// Should match even deeper
entries = matcher.Match("did1/p/a/b/c/d")
if len(entries) != 1 {
t.Errorf("did1/p/a/b/c/d should match did1/p/#, got %d entries", len(entries))
}
}
func TestMatcher_HashMustBeLast(t *testing.T) {
matcher := miot.NewMIoTMatcher()
handler := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("did1/#/1", handler, nil) // # should be last
// # in the middle should NOT match (matchTopic returns early)
entries := matcher.Match("did1/p/2/1")
if len(entries) != 0 {
t.Errorf("did1/#/1 should NOT match did1/p/2/1, got %d entries", len(entries))
}
}
// ============================================================================
// Wildcard matching: +
// ============================================================================
func TestMatcher_SingleLevelWildcard(t *testing.T) {
matcher := miot.NewMIoTMatcher()
handler := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("did1/p/+/1", handler, nil)
// + should match exactly one level
entries := matcher.Match("did1/p/2/1")
if len(entries) != 1 {
t.Errorf("did1/p/2/1 should match did1/p/+/1, got %d entries", len(entries))
}
entries = matcher.Match("did1/p/99/1")
if len(entries) != 1 {
t.Errorf("did1/p/99/1 should match did1/p/+/1, got %d entries", len(entries))
}
// + should NOT match multiple levels
entries = matcher.Match("did1/p/2/3/1")
if len(entries) != 0 {
t.Errorf("did1/p/2/3/1 should NOT match did1/p/+/1 (too deep), got %d entries", len(entries))
}
// + should NOT match when there's no level to match
entries = matcher.Match("did1/p/1")
if len(entries) != 0 {
t.Errorf("did1/p/1 should NOT match did1/p/+/1 (too shallow), got %d entries", len(entries))
}
}
// ============================================================================
// Combined wildcards
// ============================================================================
func TestMatcher_CombinedWildcards(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h1 := func(params map[string]interface{}, ctx interface{}) {}
h2 := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("did1/+/+/#", h1, nil) // Any service, any property, deep
matcher.Sub("did1/p/+/#", h2, nil) // P-service, any property, deep
// Match "did1/p/2/1" → both patterns should match
entries := matcher.Match("did1/p/2/1")
if len(entries) != 2 {
t.Errorf("did1/p/2/1 should match 2 patterns (did1/+/+/# and did1/p/+/#), got %d", len(entries))
}
// Match "did1/e/3/1" → only did1/+/+/# should match
entries = matcher.Match("did1/e/3/1")
if len(entries) != 1 {
t.Errorf("did1/e/3/1 should match 1 pattern (did1/+/+/#), got %d", len(entries))
}
}
// ============================================================================
// Negative matching
// ============================================================================
func TestMatcher_NoMatch(t *testing.T) {
matcher := miot.NewMIoTMatcher()
handler := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("did1/p/2/1", handler, nil)
// Different DID
entries := matcher.Match("did2/p/2/1")
if len(entries) != 0 {
t.Error("did2 should NOT match did1 subscriptions")
}
// Different topic
entries = matcher.Match("did1")
if len(entries) != 0 {
t.Error("'did1' should NOT be a match (too short)")
}
// Extra levels
entries = matcher.Match("did1/p/2/1/extra")
if len(entries) != 0 {
t.Error("did1/p/2/1/extra should NOT match did1/p/2/1")
}
}
// ============================================================================
// Clear and Size
// ============================================================================
func TestMatcher_Clear(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("did1/p/#", h, nil)
matcher.Sub("did2/p/#", h, nil)
if matcher.Size() != 2 {
t.Errorf("Size() = %d, want 2", matcher.Size())
}
matcher.Clear()
if matcher.Size() != 0 {
t.Errorf("Size() after Clear = %d, want 0", matcher.Size())
}
entries := matcher.Match("did1/p/2/1")
if len(entries) != 0 {
t.Error("Match should return empty after Clear")
}
}
func TestMatcher_Size(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
matcher.Sub("a/b/#", h, nil)
matcher.Sub("a/b/c", h, nil)
matcher.Sub("a/b/d", h, nil)
// Same handler registered on 3 patterns
if matcher.Size() != 3 {
t.Errorf("Size() = %d, want 3", matcher.Size())
}
}
// ============================================================================
// Unsub edge cases
// ============================================================================
func TestMatcher_UnsubNonExistent(t *testing.T) {
matcher := miot.NewMIoTMatcher()
// Unsub of non-existent ID should not panic
matcher.Unsub("nonexistent_id")
if matcher.Size() != 0 {
t.Errorf("Size() should be 0, got %d", matcher.Size())
}
}
func TestMatcher_UnsubAndRematch(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h1 := func(params map[string]interface{}, ctx interface{}) {}
h2 := func(params map[string]interface{}, ctx interface{}) {}
sub1 := matcher.Sub("did1/p/#", h1, nil)
sub2 := matcher.Sub("did1/p/#", h2, nil)
if matcher.Size() != 2 {
t.Fatalf("Size() = %d, want 2", matcher.Size())
}
// Unsub first
matcher.Unsub(sub1)
if matcher.Size() != 1 {
t.Errorf("Size() after unsub1 = %d, want 1", matcher.Size())
}
// Unsub second
matcher.Unsub(sub2)
if matcher.Size() != 0 {
t.Errorf("Size() after unsub2 = %d, want 0", matcher.Size())
}
// Match should return empty
entries := matcher.Match("did1/p/2/1")
if len(entries) != 0 {
t.Errorf("Match should return empty, got %d entries", len(entries))
}
}
// ============================================================================
// Concurrent operations
// ============================================================================
func TestMatcher_ConcurrentSubUnsub(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
var wg sync.WaitGroup
subIDs := make(chan string, 100)
// Concurrent Sub
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
subID := matcher.Sub("did1/p/#", h, nil)
subIDs <- subID
}(i)
}
wg.Wait()
close(subIDs)
// All subscriptions should complete without panic
count := 0
for range subIDs {
count++
}
if count != 50 {
t.Errorf("expected 50 subscriptions, got %d", count)
}
}
func TestMatcher_ConcurrentMatchAndUnsub(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
subID := matcher.Sub("did1/p/#", h, nil)
matcher.Sub("did1/e/#", h, nil)
var wg sync.WaitGroup
// Concurrent Match (read lock)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = matcher.Match("did1/p/2/1")
}()
}
// Concurrent Unsub (write lock)
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
matcher.Unsub(subID)
}()
wg.Wait()
// No panic = pass
}
func TestMatcher_ConcurrentMatchAndClear(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
for i := 0; i < 10; i++ {
matcher.Sub("did1/p/#", h, nil)
}
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = matcher.Match("did1/p/2/1")
}()
}
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond)
matcher.Clear()
}()
wg.Wait()
}
// ============================================================================
// Multiple subscriptions on same topic
// ============================================================================
func TestMatcher_MultipleHandlersSameTopic(t *testing.T) {
matcher := miot.NewMIoTMatcher()
called := make(chan string, 3)
h1 := func(params map[string]interface{}, ctx interface{}) { called <- "h1" }
h2 := func(params map[string]interface{}, ctx interface{}) { called <- "h2" }
h3 := func(params map[string]interface{}, ctx interface{}) { called <- "h3" }
matcher.Sub("did1/p/2/1", h1, nil)
matcher.Sub("did1/p/2/1", h2, nil)
matcher.Sub("did1/p/2/1", h3, nil)
entries := matcher.Match("did1/p/2/1")
if len(entries) != 3 {
t.Errorf("expected 3 entries for same topic, got %d", len(entries))
}
// Call all handlers
for _, e := range entries {
if e.Handler != nil {
e.Handler(nil, e.Ctx)
}
}
close(called)
count := 0
for range called {
count++
}
if count != 3 {
t.Errorf("expected 3 handler calls, got %d", count)
}
}
// ============================================================================
// Empty/new matcher
// ============================================================================
func TestMatcher_EmptyMatcher(t *testing.T) {
matcher := miot.NewMIoTMatcher()
if matcher.Size() != 0 {
t.Errorf("new matcher size = %d, want 0", matcher.Size())
}
entries := matcher.Match("anything")
if len(entries) != 0 {
t.Errorf("empty matcher match = %d entries, want 0", len(entries))
}
// Clear on empty matcher should not panic
matcher.Clear()
}
// ============================================================================
// Pattern with trailing slash
// ============================================================================
func TestMatcher_TrailingSlash(t *testing.T) {
matcher := miot.NewMIoTMatcher()
h := func(params map[string]interface{}, ctx interface{}) {}
// Subscription pattern and match topic should be consistent
matcher.Sub("did1/p/2/1", h, nil)
// Exact match
entries := matcher.Match("did1/p/2/1")
if len(entries) != 1 {
t.Errorf("exact match failed, got %d entries", len(entries))
}
}