Files
xiaomihome/miot/miot_test.go
T
4566704 b808e51b3e refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志
主要变更:

**类型系统重构**
- HomeInfo.ID/RoomInfo.ID/HomeID 从 string 改为 int64,json tag 使用 ,string
- GetHome/GetRooms/GetRoomDevices 接口同步修改
- 新增 int64FromStr 工具函数,更新 parseHomeInfo 解析逻辑

**新增 Speaker 设备**
- 新增 KindSpeaker 类型
- classify 映射覆盖 speaker/bluetooth-speaker/alarm-clock/media-player/playback/music

**设备驱动重构**
- AC 驱动彻底重写:从硬编码常量改为基于 SPEC ValueList 的动态 valMapper
- 所有设备(Fan/Humidifier/Light/OccupancySensor/Switch/TempHumiditySensor/Thermostat/Vacuum/WaterHeater/Cover)新增 GetState() 和 FetchState(ctx) 接口
- base.go 新增 specResolver.format() 方法

**MIoT 模块增强**
- MIoTClient 新增 homeIDs 字段,支持按家庭过滤设备
- RefreshDevices 支持 homeIDs 参数
- SpecStdLib 引入 logger.Logger,log.Printf 全部迁移为结构化日志

**其他**
- Config 新增 RedirectURL 和 HomeIDs 字段
- AuthInfo.ExpiresIn 类型从 int 改为 int32
- UUID 生成简化为 hex.EncodeToString
- 新增 github.com/sirupsen/logrus 依赖
- .gitignore 新增 examples/*/data/ 忽略规则
- 删除已提交的 config.json 敏感配置
2026-07-13 10:40:20 +08:00

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(nil)
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)
}