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