go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为 "xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目 介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构 设计文档。
405 lines
11 KiB
Go
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))
|
|
}
|
|
}
|