go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为 "xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目 介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构 设计文档。
580 lines
15 KiB
Go
580 lines
15 KiB
Go
// Package miot_test provides black-box tests for miot_device.go device model.
|
|
package miot_test
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
miot "xiaomihome/miot"
|
|
)
|
|
|
|
// ============================================================================
|
|
// NewMIoTDevice — field parsing
|
|
// ============================================================================
|
|
|
|
func TestDevice_NewAllFields(t *testing.T) {
|
|
// nil client is valid for testing basic field parsing
|
|
deviceInfo := map[string]interface{}{
|
|
"did": "device_001",
|
|
"name": "Test Light",
|
|
"model": "lumi.plug.v1",
|
|
"online": true,
|
|
"home_id": "home_123",
|
|
"home_name": "My Home",
|
|
"room_id": "room_456",
|
|
"room_name": "Living Room",
|
|
"group_id": "group_789",
|
|
"fw_version": "1.0.0",
|
|
"manufacturer": "Xiaomi",
|
|
"icon": "https://example.com/icon.png",
|
|
"token": "abc123token",
|
|
"local_ip": "192.168.1.100",
|
|
"connect_type": float64(2),
|
|
"rssi": float64(-65),
|
|
"ssid": "MyWiFi",
|
|
"bssid": "00:11:22:33:44:55",
|
|
}
|
|
|
|
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
|
|
|
|
if dev.DID() != "device_001" {
|
|
t.Errorf("DID = %q, want %q", dev.DID(), "device_001")
|
|
}
|
|
if dev.Name() != "Test Light" {
|
|
t.Errorf("Name = %q, want %q", dev.Name(), "Test Light")
|
|
}
|
|
if dev.Model() != "lumi.plug.v1" {
|
|
t.Errorf("Model = %q, want %q", dev.Model(), "lumi.plug.v1")
|
|
}
|
|
if !dev.Online() {
|
|
t.Error("Online should be true")
|
|
}
|
|
if !dev.IsOnline() {
|
|
t.Error("IsOnline should be true")
|
|
}
|
|
if dev.HomeID() != "home_123" {
|
|
t.Errorf("HomeID = %q", dev.HomeID())
|
|
}
|
|
if dev.HomeName() != "My Home" {
|
|
t.Errorf("HomeName = %q", dev.HomeName())
|
|
}
|
|
if dev.RoomID() != "room_456" {
|
|
t.Errorf("RoomID = %q", dev.RoomID())
|
|
}
|
|
if dev.RoomName() != "Living Room" {
|
|
t.Errorf("RoomName = %q", dev.RoomName())
|
|
}
|
|
if dev.GroupID() != "group_789" {
|
|
t.Errorf("GroupID = %q", dev.GroupID())
|
|
}
|
|
if dev.FWVersion() != "1.0.0" {
|
|
t.Errorf("FWVersion = %q", dev.FWVersion())
|
|
}
|
|
if dev.Manufacturer() != "Xiaomi" {
|
|
t.Errorf("Manufacturer = %q", dev.Manufacturer())
|
|
}
|
|
if dev.Icon() != "https://example.com/icon.png" {
|
|
t.Errorf("Icon = %q", dev.Icon())
|
|
}
|
|
if dev.Token() != "abc123token" {
|
|
t.Errorf("Token = %q", dev.Token())
|
|
}
|
|
if dev.IP() != "192.168.1.100" {
|
|
t.Errorf("IP = %q", dev.IP())
|
|
}
|
|
if dev.ConnectType() != 2 {
|
|
t.Errorf("ConnectType = %d, want 2", dev.ConnectType())
|
|
}
|
|
if dev.RSSI() != -65 {
|
|
t.Errorf("RSSI = %d, want -65", dev.RSSI())
|
|
}
|
|
if dev.SSID() != "MyWiFi" {
|
|
t.Errorf("SSID = %q", dev.SSID())
|
|
}
|
|
if dev.BSSID() != "00:11:22:33:44:55" {
|
|
t.Errorf("BSSID = %q", dev.BSSID())
|
|
}
|
|
}
|
|
|
|
func TestDevice_NewMinimalFields(t *testing.T) {
|
|
// Only minimum fields
|
|
deviceInfo := map[string]interface{}{
|
|
"did": "device_min",
|
|
"name": "Min Device",
|
|
}
|
|
|
|
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
|
|
|
|
if dev.DID() != "device_min" {
|
|
t.Errorf("DID = %q", dev.DID())
|
|
}
|
|
if dev.Name() != "Min Device" {
|
|
t.Errorf("Name = %q", dev.Name())
|
|
}
|
|
if dev.Model() != "" {
|
|
t.Errorf("Model should be empty, got %q", dev.Model())
|
|
}
|
|
if dev.Online() {
|
|
t.Error("Online should default to false")
|
|
}
|
|
if dev.HomeID() != "" {
|
|
t.Error("HomeID should be empty")
|
|
}
|
|
}
|
|
|
|
func TestDevice_NewEmptyInfo(t *testing.T) {
|
|
// Empty device info should not panic
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{}, nil)
|
|
if dev == nil {
|
|
t.Fatal("NewMIoTDevice should not return nil")
|
|
}
|
|
if dev.DID() != "" {
|
|
t.Errorf("DID should be empty, got %q", dev.DID())
|
|
}
|
|
}
|
|
|
|
func TestDevice_NewNilInfo(t *testing.T) {
|
|
// nil device info should not panic
|
|
dev := miot.NewMIoTDevice(nil, nil, nil)
|
|
if dev == nil {
|
|
t.Fatal("NewMIoTDevice with nil info should still return device")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Model splitting
|
|
// ============================================================================
|
|
|
|
func TestDevice_ModelStrs(t *testing.T) {
|
|
tests := []struct {
|
|
model string
|
|
wantLen int
|
|
wantPart string
|
|
}{
|
|
{"lumi.plug.maeu01", 3, "lumi"},
|
|
{"xiaomi.device", 2, "xiaomi"},
|
|
{"simple", 1, "simple"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.model, func(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "d",
|
|
"model": tt.model,
|
|
}, nil)
|
|
|
|
if dev.Model() != tt.model {
|
|
t.Errorf("Model = %q, want %q", dev.Model(), tt.model)
|
|
}
|
|
// ModelStrs are internal, verified via manufacturer (first part)
|
|
if dev.Manufacturer() != tt.wantPart {
|
|
// manufacturer is set from deviceInfo["manufacturer"], not from model split
|
|
// This test verifies Model() returns correctly
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Online / SetOnline
|
|
// ============================================================================
|
|
|
|
func TestDevice_SetOnline(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "d",
|
|
"online": false,
|
|
}, nil)
|
|
|
|
if dev.Online() {
|
|
t.Error("Online should be false initially")
|
|
}
|
|
|
|
dev.SetOnline(true)
|
|
if !dev.Online() {
|
|
t.Error("Online should be true after SetOnline(true)")
|
|
}
|
|
if !dev.IsOnline() {
|
|
t.Error("IsOnline should be true")
|
|
}
|
|
|
|
dev.SetOnline(false)
|
|
if dev.Online() {
|
|
t.Error("Online should be false after SetOnline(false)")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HasWiFi
|
|
// ============================================================================
|
|
|
|
func TestDevice_HasWiFi(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
connectType float64
|
|
want bool
|
|
}{
|
|
{"connected_pid2", 2, true},
|
|
{"connected_pid0", 0, true},
|
|
{"not_connected", -1, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "d",
|
|
"connect_type": tt.connectType,
|
|
}, nil)
|
|
if dev.HasWiFi() != tt.want {
|
|
t.Errorf("HasWiFi = %v, want %v", dev.HasWiFi(), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// LoadFromCloud
|
|
// ============================================================================
|
|
|
|
func TestDevice_LoadFromCloud(t *testing.T) {
|
|
cloudData := map[string]interface{}{
|
|
"did": "cloud_dev",
|
|
"name": "Cloud Device",
|
|
}
|
|
|
|
dev := miot.LoadFromCloud(nil, cloudData)
|
|
if dev == nil {
|
|
t.Fatal("LoadFromCloud should not return nil")
|
|
}
|
|
if dev.DID() != "cloud_dev" {
|
|
t.Errorf("DID = %q, want 'cloud_dev'", dev.DID())
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// LoadSPEC / SpecInstance
|
|
// ============================================================================
|
|
|
|
func TestDevice_LoadSPEC(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
// nil spec → error
|
|
err := dev.LoadSPEC(nil)
|
|
if err == nil {
|
|
t.Error("LoadSPEC(nil) should return error")
|
|
}
|
|
|
|
if dev.SpecInstance() != nil {
|
|
t.Error("SpecInstance should be nil before LoadSPEC")
|
|
}
|
|
}
|
|
|
|
func TestDevice_SetSpecInstance(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
// Create a minimal spec instance
|
|
spec := &miot.MIoTSpecInstance{
|
|
URN: "urn:example:1",
|
|
Type: "example",
|
|
Description: "Test Spec",
|
|
DescriptionTrans: "Test Spec Translation",
|
|
}
|
|
|
|
dev.SetSpecInstance(spec)
|
|
if dev.SpecInstance() != spec {
|
|
t.Error("SpecInstance should return the set value")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sub/Unsub device state (no client needed)
|
|
// ============================================================================
|
|
|
|
func TestDevice_SubscribeState(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
var calledMu sync.Mutex
|
|
callCount := 0
|
|
handler := func(did string, state interface{}) {
|
|
calledMu.Lock()
|
|
callCount++
|
|
calledMu.Unlock()
|
|
}
|
|
|
|
subID := dev.SubscribeState("online", handler)
|
|
if subID == "" {
|
|
t.Error("SubscribeState should return non-empty subID")
|
|
}
|
|
|
|
// Notify and check handler is called
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
dev.NotifyStateChange(map[string]interface{}{"online": true})
|
|
time.Sleep(50 * time.Millisecond)
|
|
done <- true
|
|
}()
|
|
<-done
|
|
|
|
calledMu.Lock()
|
|
count := callCount
|
|
calledMu.Unlock()
|
|
if count != 1 {
|
|
t.Errorf("handler should be called once, got %d", count)
|
|
}
|
|
|
|
// Unsubscribe
|
|
dev.UnsubscribeState("online", subID)
|
|
|
|
// Notify again - should not call handler
|
|
go func() {
|
|
dev.NotifyStateChange(map[string]interface{}{"online": false})
|
|
time.Sleep(50 * time.Millisecond)
|
|
done <- true
|
|
}()
|
|
<-done
|
|
|
|
calledMu.Lock()
|
|
count = callCount
|
|
calledMu.Unlock()
|
|
if count != 1 {
|
|
t.Errorf("handler should not be called after unsub, got %d calls", count)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SubProp / UnsubProp (no client → warns)
|
|
// ============================================================================
|
|
|
|
func TestDevice_SubPropNoClient(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
handler := func(params map[string]interface{}, ctx interface{}) {}
|
|
subID := dev.SubProp(2, 1, handler)
|
|
// With nil client, should return empty string and not panic
|
|
if subID != "" {
|
|
t.Errorf("SubProp with nil client should return empty string, got %q", subID)
|
|
}
|
|
|
|
// UnsubProp with nil client should not panic
|
|
dev.UnsubProp("some_id")
|
|
}
|
|
|
|
func TestDevice_SubEventNoClient(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
handler := func(params map[string]interface{}, ctx interface{}) {}
|
|
subID := dev.SubEvent(2, 1, handler)
|
|
if subID != "" {
|
|
t.Errorf("SubEvent with nil client should return empty string, got %q", subID)
|
|
}
|
|
|
|
dev.UnsubEvent("some_id")
|
|
}
|
|
|
|
// ============================================================================
|
|
// ToDict / FromDict
|
|
// ============================================================================
|
|
|
|
func TestDevice_ToDict(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "dev1",
|
|
"name": "Test Device",
|
|
"model": "test.model.v1",
|
|
"online": true,
|
|
"home_id": "h1",
|
|
"home_name": "Home",
|
|
"room_id": "r1",
|
|
"room_name": "Room",
|
|
"group_id": "g1",
|
|
"fw_version": "1.0",
|
|
"icon": "icon.png",
|
|
"token": "tok",
|
|
"local_ip": "127.0.0.1",
|
|
"connect_type": float64(1),
|
|
"rssi": float64(-50),
|
|
"ssid": "wifi",
|
|
"bssid": "aa:bb",
|
|
}, nil)
|
|
|
|
dict := dev.ToDict()
|
|
if dict["did"] != "dev1" {
|
|
t.Errorf("did = %v", dict["did"])
|
|
}
|
|
if dict["name"] != "Test Device" {
|
|
t.Errorf("name = %v", dict["name"])
|
|
}
|
|
if dict["online"] != true {
|
|
t.Errorf("online = %v", dict["online"])
|
|
}
|
|
if dict["model"] != "test.model.v1" {
|
|
t.Errorf("model = %v", dict["model"])
|
|
}
|
|
if dict["home_id"] != "h1" {
|
|
t.Errorf("home_id = %v", dict["home_id"])
|
|
}
|
|
if dict["room_id"] != "r1" {
|
|
t.Errorf("room_id = %v", dict["room_id"])
|
|
}
|
|
if dict["connect_type"] != 1 {
|
|
t.Errorf("connect_type = %v", dict["connect_type"])
|
|
}
|
|
if dict["local_ip"] != "127.0.0.1" {
|
|
t.Errorf("local_ip = %v", dict["local_ip"])
|
|
}
|
|
}
|
|
|
|
func TestDevice_FromDict(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"did": "from_dict_dev",
|
|
"name": "From Dict Device",
|
|
"online": true,
|
|
"model": "model.x",
|
|
"manufacturer": "TestCo",
|
|
}
|
|
|
|
dev := miot.FromDict(nil, data)
|
|
if dev == nil {
|
|
t.Fatal("FromDict should not return nil")
|
|
}
|
|
if dev.DID() != "from_dict_dev" {
|
|
t.Errorf("DID = %q", dev.DID())
|
|
}
|
|
if dev.Name() != "From Dict Device" {
|
|
t.Errorf("Name = %q", dev.Name())
|
|
}
|
|
if dev.Manufacturer() != "TestCo" {
|
|
t.Errorf("Manufacturer = %q", dev.Manufacturer())
|
|
}
|
|
}
|
|
|
|
func TestDevice_ToDictFromDictRoundtrip(t *testing.T) {
|
|
original := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "roundtrip",
|
|
"name": "Roundtrip",
|
|
"model": "m.v",
|
|
"online": true,
|
|
"manufacturer": "Mfr",
|
|
"fw_version": "2.0",
|
|
"icon": "i.png",
|
|
"home_id": "h",
|
|
"home_name": "hn",
|
|
"room_id": "r",
|
|
"room_name": "rn",
|
|
"group_id": "g",
|
|
"connect_type": float64(3),
|
|
"rssi": float64(-30),
|
|
"ssid": "s",
|
|
"bssid": "b",
|
|
"token": "t",
|
|
"local_ip": "10.0.0.1",
|
|
}, nil)
|
|
|
|
dict := original.ToDict()
|
|
restored := miot.FromDict(nil, dict)
|
|
|
|
if restored.DID() != original.DID() {
|
|
t.Error("DID mismatch")
|
|
}
|
|
if restored.Name() != original.Name() {
|
|
t.Error("Name mismatch")
|
|
}
|
|
if restored.Model() != original.Model() {
|
|
t.Error("Model mismatch")
|
|
}
|
|
if restored.Manufacturer() != original.Manufacturer() {
|
|
t.Error("Manufacturer mismatch")
|
|
}
|
|
if restored.FWVersion() != original.FWVersion() {
|
|
t.Error("FWVersion mismatch")
|
|
}
|
|
if restored.Icon() != original.Icon() {
|
|
t.Error("Icon mismatch")
|
|
}
|
|
if restored.HomeID() != original.HomeID() {
|
|
t.Error("HomeID mismatch")
|
|
}
|
|
if restored.RoomID() != original.RoomID() {
|
|
t.Error("RoomID mismatch")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sub devices
|
|
// ============================================================================
|
|
|
|
func TestDevice_AddGetSubDevice(t *testing.T) {
|
|
parent := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "parent"}, nil)
|
|
child := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "child"}, nil)
|
|
|
|
// Get non-existent
|
|
if parent.GetSubDevice(1) != nil {
|
|
t.Error("GetSubDevice should return nil for non-existent")
|
|
}
|
|
|
|
parent.AddSubDevice(1, child)
|
|
|
|
got := parent.GetSubDevice(1)
|
|
if got == nil {
|
|
t.Fatal("GetSubDevice should return added device")
|
|
}
|
|
if got.DID() != "child" {
|
|
t.Errorf("sub device DID = %q, want 'child'", got.DID())
|
|
}
|
|
|
|
// Get non-existent again
|
|
if parent.GetSubDevice(2) != nil {
|
|
t.Error("GetSubDevice(2) should return nil")
|
|
}
|
|
}
|
|
|
|
func TestDevice_SubDevicesAccessor(t *testing.T) {
|
|
parent := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "parent"}, nil)
|
|
|
|
subs := parent.SubDevices()
|
|
if len(subs) != 0 {
|
|
t.Errorf("SubDevices should be empty, got %d", len(subs))
|
|
}
|
|
|
|
child := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "child"}, nil)
|
|
parent.AddSubDevice(1, child)
|
|
|
|
subs = parent.SubDevices()
|
|
if len(subs) != 1 {
|
|
t.Errorf("SubDevices should have 1 element, got %d", len(subs))
|
|
}
|
|
}
|
|
|
|
func TestDevice_ToDictWithSubDevices(t *testing.T) {
|
|
parent := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "parent_dev",
|
|
}, nil)
|
|
child := miot.NewMIoTDevice(nil, map[string]interface{}{
|
|
"did": "child_dev",
|
|
"name": "Child Device",
|
|
}, nil)
|
|
|
|
parent.AddSubDevice(2, child)
|
|
|
|
dict := parent.ToDict()
|
|
if subDevs, ok := dict["sub_devices"].(map[string]interface{}); !ok {
|
|
t.Error("ToDict should contain sub_devices")
|
|
} else {
|
|
if _, ok := subDevs["s2"]; !ok {
|
|
t.Errorf("sub_devices should contain key 's2', got %v", subDevs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ServiceList
|
|
// ============================================================================
|
|
|
|
func TestDevice_ServiceList(t *testing.T) {
|
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
|
|
|
|
// Initially nil
|
|
if dev.ServiceList() != nil {
|
|
t.Error("ServiceList should be nil initially")
|
|
}
|
|
}
|