feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
// 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 "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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user