Files
4566704 2b7de69403 refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口
为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
2026-09-15 20:16:26 +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)
}