- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
263 lines
7.4 KiB
Go
263 lines
7.4 KiB
Go
// Package miot provides tests for MIoT core client.
|
|
package miot
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Test Group 1: MIoTMatcher subscription tree
|
|
// ============================================================================
|
|
|
|
func TestMIoTMatcher_SubMatch(t *testing.T) {
|
|
matcher := NewMIoTMatcher()
|
|
|
|
// Test exact match
|
|
handler1 := func(params map[string]interface{}, ctx interface{}) {
|
|
fmt.Println("handler1 called")
|
|
}
|
|
subID1 := matcher.Sub("did1/p/2/1", handler1, nil)
|
|
|
|
entries := matcher.Match("did1/p/2/1")
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 match, got %d", len(entries))
|
|
}
|
|
|
|
// Test wildcard match
|
|
handler2 := func(params map[string]interface{}, ctx interface{}) {
|
|
fmt.Println("handler2 called")
|
|
}
|
|
subID2 := matcher.Sub("did1/p/#", handler2, nil)
|
|
|
|
entries = matcher.Match("did1/p/2/1")
|
|
if len(entries) != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", len(entries))
|
|
}
|
|
|
|
// Test unsubscribe
|
|
matcher.Unsub(subID1)
|
|
entries = matcher.Match("did1/p/2/1")
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 match after unsub, got %d", len(entries))
|
|
}
|
|
|
|
// Clean up
|
|
matcher.Unsub(subID2)
|
|
|
|
t.Log("TestMIoTMatcher_SubMatch passed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 2: Device management
|
|
// ============================================================================
|
|
|
|
func TestMIoTClient_LoadDevices(t *testing.T) {
|
|
// Create client
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
defer client.Stop()
|
|
|
|
// Set storage
|
|
storage := NewMIoTStorage("./test_data")
|
|
client.SetStorage(storage)
|
|
|
|
// Initialize
|
|
if err := client.Init(); err != nil {
|
|
t.Logf("Init error (expected in test): %v", err)
|
|
}
|
|
|
|
// Check device list format
|
|
deviceList := client.DeviceList()
|
|
if deviceList == nil {
|
|
t.Error("Device list should not be nil")
|
|
}
|
|
|
|
t.Log("TestMIoTClient_LoadDevices passed")
|
|
}
|
|
|
|
func TestMIoTClient_RefreshDevices(t *testing.T) {
|
|
// Create client
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
defer client.Stop()
|
|
|
|
// Set storage
|
|
storage := NewMIoTStorage("./test_data")
|
|
client.SetStorage(storage)
|
|
|
|
// Refresh devices (will fail without proper HTTP client, but should not panic)
|
|
err := client.RefreshDevices()
|
|
if err != nil {
|
|
t.Logf("RefreshDevices error (expected in test): %v", err)
|
|
}
|
|
|
|
t.Log("TestMIoTClient_RefreshDevices passed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 3: Property refresh
|
|
// ============================================================================
|
|
|
|
func TestMIoTClient_RequestRefreshProp(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
|
|
// Request refresh
|
|
client.RequestRefreshProp("did1", 2, 1)
|
|
client.RequestRefreshProp("did1", 2, 1) // Duplicate, should be skipped
|
|
|
|
// Check refresh list
|
|
// Note: This is a simplified test, in real scenario we would check the internal state
|
|
|
|
// Clean up: stop the timer to avoid affecting other tests
|
|
client.mu.Lock()
|
|
if client.refreshPropsTimer != nil {
|
|
client.refreshPropsTimer.Stop()
|
|
client.refreshPropsTimer = nil
|
|
}
|
|
client.mu.Unlock()
|
|
|
|
t.Log("TestMIoTClient_RequestRefreshProp passed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 4: MQTT message handling
|
|
// ============================================================================
|
|
|
|
func TestMIoTClient_OnPropMsg(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
defer client.Stop()
|
|
|
|
// Use a channel to wait for the handler to be called
|
|
handlerCalled := make(chan bool, 1)
|
|
|
|
// Subscribe to property directly (avoid SubProp which starts timer)
|
|
handler := func(params map[string]interface{}, ctx interface{}) {
|
|
t.Logf("Property changed: %v", params)
|
|
handlerCalled <- true
|
|
}
|
|
// Subscribe to wildcard pattern that matches all properties for did1
|
|
client.subTree.Sub("did1/p/#", handler, nil)
|
|
|
|
// Simulate property message
|
|
// OnPropMsg expects params with "params" containing did, siid, piid, value
|
|
params := map[string]interface{}{
|
|
"params": map[string]interface{}{
|
|
"did": "did1",
|
|
"siid": float64(2),
|
|
"piid": float64(1),
|
|
"value": true,
|
|
},
|
|
}
|
|
client.OnPropMsg(params, nil)
|
|
|
|
// Wait for handler to be called (with timeout)
|
|
select {
|
|
case <-handlerCalled:
|
|
t.Log("Handler was called successfully")
|
|
case <-time.After(1 * time.Second):
|
|
t.Error("Handler should have been called")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 5: Device state changes
|
|
// ============================================================================
|
|
|
|
func TestMIoTClient_DeviceStateChanged(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
defer client.Stop()
|
|
|
|
// Run OnDeviceStateChanged in a goroutine with timeout
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
// Simulate device state change
|
|
client.OnDeviceStateChanged("did1", map[string]interface{}{"online": true})
|
|
done <- true
|
|
}()
|
|
|
|
// Wait for completion or timeout
|
|
select {
|
|
case <-done:
|
|
t.Log("TestMIoTClient_DeviceStateChanged passed")
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("OnDeviceStateChanged timed out - possible deadlock")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 6: Concurrency safety
|
|
// ============================================================================
|
|
|
|
func TestMIoTClient_ConcurrentRefresh(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
|
|
// Concurrent refresh (should not panic)
|
|
done := make(chan bool)
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
client.RequestRefreshProp("did1", 2, 1)
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
|
|
t.Log("TestMIoTClient_ConcurrentRefresh passed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Group 7: Align Python behavior
|
|
// ============================================================================
|
|
|
|
func TestAlignPython_NoCentralPropCache(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
|
|
// Verify: MIoTClient does not store property cache
|
|
// The client should dispatch property changes via MIoTMatcher, not store them
|
|
_ = client
|
|
|
|
t.Log("TestAlignPython_NoCentralPropCache passed")
|
|
}
|
|
|
|
func TestAlignPython_DeviceListFormat(t *testing.T) {
|
|
client := NewMIoTClient("test", nil, "test_uid", "cn", CtrlModeCloud)
|
|
|
|
// Verify: deviceListCache stores map[string]interface{} (aligned with Python dict)
|
|
client.deviceListMu.Lock()
|
|
client.deviceListCache["did1"] = map[string]interface{}{
|
|
"did": "did1",
|
|
"name": "Test Device",
|
|
"online": false,
|
|
}
|
|
client.deviceListMu.Unlock()
|
|
|
|
deviceInfo := client.GetDeviceInfo("did1")
|
|
if deviceInfo == nil {
|
|
t.Error("Device info should not be nil")
|
|
}
|
|
|
|
if name, ok := deviceInfo["name"].(string); !ok || name != "Test Device" {
|
|
t.Errorf("Expected device name 'Test Device', got %v", name)
|
|
}
|
|
|
|
t.Log("TestAlignPython_DeviceListFormat passed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Cleanup
|
|
// ============================================================================
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Run tests
|
|
code := m.Run()
|
|
|
|
// Cleanup
|
|
os.RemoveAll("./test_data")
|
|
|
|
os.Exit(code)
|
|
}
|