// 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)) } }