feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
// Package miot_test provides black-box tests for spec_parser.go SPEC parsing.
|
||||
package miot_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
miot "miot"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// NewSpecStdLib
|
||||
// ============================================================================
|
||||
|
||||
func TestSpec_NewSpecStdLib(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
if lib == nil {
|
||||
t.Fatal("NewSpecStdLib returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_NewSpecStdLibEmptyLang(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("")
|
||||
if lib == nil {
|
||||
t.Fatal("NewSpecStdLib with empty lang should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpecStdLib.Load / Dump
|
||||
// ============================================================================
|
||||
|
||||
func TestSpec_LoadDumpRoundtrip(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
|
||||
input := map[string]interface{}{
|
||||
"devices": map[string]interface{}{
|
||||
"urn:example:device:1": map[string]interface{}{
|
||||
"en": "Example Device",
|
||||
"zh-Hans": "示例设备",
|
||||
},
|
||||
},
|
||||
"services": map[string]interface{}{
|
||||
"urn:example:service:1": map[string]interface{}{
|
||||
"en": "Example Service",
|
||||
},
|
||||
},
|
||||
"properties": map[string]interface{}{
|
||||
"urn:example:prop:1": map[string]interface{}{
|
||||
"en": "Example Property",
|
||||
},
|
||||
},
|
||||
"events": map[string]interface{}{
|
||||
"urn:example:event:1": map[string]interface{}{
|
||||
"en": "Example Event",
|
||||
},
|
||||
},
|
||||
"actions": map[string]interface{}{
|
||||
"urn:example:action:1": map[string]interface{}{
|
||||
"en": "Example Action",
|
||||
},
|
||||
},
|
||||
"values": map[string]interface{}{
|
||||
"urn:example:value:1": map[string]interface{}{
|
||||
"en": "Example Value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lib.Load(input)
|
||||
dumped := lib.Dump()
|
||||
|
||||
if len(dumped) != 6 {
|
||||
t.Errorf("Dump should have 6 keys, got %d", len(dumped))
|
||||
}
|
||||
for _, key := range []string{"devices", "services", "properties", "events", "actions", "values"} {
|
||||
if _, ok := dumped[key]; !ok {
|
||||
t.Errorf("Dump missing key %q", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_LoadNil(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(nil) // should not panic
|
||||
}
|
||||
|
||||
func TestSpec_LoadEmpty(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{}) // should not panic
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpecStdLib translations
|
||||
// ============================================================================
|
||||
|
||||
func TestSpec_DeviceTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("zh-Hans")
|
||||
lib.Load(map[string]interface{}{
|
||||
"devices": map[string]interface{}{
|
||||
"urn:example:dev:1": map[string]interface{}{
|
||||
"en": "Example Device",
|
||||
"zh-Hans": "示例设备",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Hit in preferred language
|
||||
result := lib.DeviceTranslate("urn:example:dev:1")
|
||||
if result != "示例设备" {
|
||||
t.Errorf("DeviceTranslate = %q, want %q", result, "示例设备")
|
||||
}
|
||||
|
||||
// Miss
|
||||
result = lib.DeviceTranslate("nonexistent")
|
||||
if result != "" {
|
||||
t.Errorf("DeviceTranslate for missing key = %q, want empty", result)
|
||||
}
|
||||
|
||||
// Empty key
|
||||
result = lib.DeviceTranslate("")
|
||||
if result != "" {
|
||||
t.Errorf("DeviceTranslate for empty key = %q, want empty", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_ServiceTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"services": map[string]interface{}{
|
||||
"urn:example:svc:1": map[string]interface{}{
|
||||
"en": "Test Service",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.ServiceTranslate("urn:example:svc:1")
|
||||
if result != "Test Service" {
|
||||
t.Errorf("ServiceTranslate = %q, want 'Test Service'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_PropertyTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"properties": map[string]interface{}{
|
||||
"urn:example:prop:1": map[string]interface{}{
|
||||
"en": "Test Property",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.PropertyTranslate("urn:example:prop:1")
|
||||
if result != "Test Property" {
|
||||
t.Errorf("PropertyTranslate = %q, want 'Test Property'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_EventTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"events": map[string]interface{}{
|
||||
"urn:example:evt:1": map[string]interface{}{
|
||||
"en": "Test Event",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.EventTranslate("urn:example:evt:1")
|
||||
if result != "Test Event" {
|
||||
t.Errorf("EventTranslate = %q, want 'Test Event'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_ActionTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"actions": map[string]interface{}{
|
||||
"urn:example:act:1": map[string]interface{}{
|
||||
"en": "Test Action",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.ActionTranslate("urn:example:act:1")
|
||||
if result != "Test Action" {
|
||||
t.Errorf("ActionTranslate = %q, want 'Test Action'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_ValueTranslate(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"values": map[string]interface{}{
|
||||
"urn:example:val:1": map[string]interface{}{
|
||||
"en": "Test Value",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.ValueTranslate("urn:example:val:1")
|
||||
if result != "Test Value" {
|
||||
t.Errorf("ValueTranslate = %q, want 'Test Value'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_TranslateFallbackToDefault(t *testing.T) {
|
||||
lib := miot.NewSpecStdLib("ja") // Japanese not in data
|
||||
lib.Load(map[string]interface{}{
|
||||
"devices": map[string]interface{}{
|
||||
"urn:example:dev:1": map[string]interface{}{
|
||||
"en": "Fallback Device",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Should fallback to default language (en)
|
||||
result := lib.DeviceTranslate("urn:example:dev:1")
|
||||
if result != "Fallback Device" {
|
||||
t.Errorf("Fallback translate = %q, want 'Fallback Device'", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpecHash
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecHash(t *testing.T) {
|
||||
h1 := miot.SpecHash("test.type", 1, 2)
|
||||
h2 := miot.SpecHash("test.type", 1, 2)
|
||||
h3 := miot.SpecHash("test.type", 1, 3)
|
||||
|
||||
if h1 != h2 {
|
||||
t.Error("SpecHash should be deterministic")
|
||||
}
|
||||
if h1 == h3 {
|
||||
t.Error("Different iids should produce different hashes")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpecFormatGoType
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecFormatGoType(t *testing.T) {
|
||||
tests := []struct {
|
||||
format string
|
||||
want string
|
||||
}{
|
||||
{"string", "str"},
|
||||
{"str", "str"},
|
||||
{"bool", "bool"},
|
||||
{"float", "float"},
|
||||
{"int", "int"},
|
||||
{"uint8", "int"},
|
||||
{"unknown", "int"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.format, func(t *testing.T) {
|
||||
got := miot.SpecFormatGoType(tt.format)
|
||||
if got != tt.want {
|
||||
t.Errorf("SpecFormatGoType(%q) = %q, want %q", tt.format, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpecPrecision
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecPrecision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
step interface{}
|
||||
want int
|
||||
}{
|
||||
{"int_0", 0, 0},
|
||||
{"int_1", 1, 0},
|
||||
{"float_0_1", 0.1, 1},
|
||||
{"float_0_5", 0.5, 1},
|
||||
{"float_0_01", 0.01, 2},
|
||||
{"float_1_0", 1.0, 0},
|
||||
{"float64_0_001", float64(0.001), 3},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := miot.SpecPrecision(tt.step)
|
||||
if got != tt.want {
|
||||
t.Errorf("SpecPrecision(%v) = %d, want %d", tt.step, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NewMIoTSpecParser
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecParser_New(t *testing.T) {
|
||||
s := miot.NewMIoTStorage(t.TempDir())
|
||||
p := miot.NewMIoTSpecParser("zh-Hans", s, "/tmp/specs")
|
||||
if p == nil {
|
||||
t.Fatal("NewMIoTSpecParser returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecParser_NewEmptyLang(t *testing.T) {
|
||||
s := miot.NewMIoTStorage(t.TempDir())
|
||||
p := miot.NewMIoTSpecParser("", s, "/tmp/specs")
|
||||
if p == nil {
|
||||
t.Fatal("NewMIoTSpecParser with empty lang should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MIoTSpecParser.Init / Parse (with spec file)
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecParser_ParseWithSpecFile(t *testing.T) {
|
||||
specDir := t.TempDir()
|
||||
|
||||
// Create a spec JSON file
|
||||
specJSON := `{
|
||||
"type": "urn:miot-spec-v2:device:example:00000001:test-device:1",
|
||||
"description": "Test Device Spec",
|
||||
"services": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:service:switch:00000001:test-switch:1",
|
||||
"description": "Switch Service",
|
||||
"properties": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:property:on:00000001:test-on:1",
|
||||
"description": "Switch Status",
|
||||
"format": "bool",
|
||||
"access": ["read", "write", "notify"]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:event:click:00000001:test-click:1",
|
||||
"description": "Click Event",
|
||||
"arguments": []
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:action:toggle:00000001:test-toggle:1",
|
||||
"description": "Toggle",
|
||||
"in": [],
|
||||
"out": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
urn := "urn:miot-spec-v2:device:example:00000001:test-device:1"
|
||||
// On Windows, colons in filenames are replaced
|
||||
fileName := urn
|
||||
if os.PathSeparator == '\\' {
|
||||
fileName = "urn_miot-spec-v2_device_example_00000001_test-device_1"
|
||||
}
|
||||
os.WriteFile(filepath.Join(specDir, fileName+".json"), []byte(specJSON), 0644)
|
||||
|
||||
s := miot.NewMIoTStorage(t.TempDir())
|
||||
p := miot.NewMIoTSpecParser("en", s, specDir)
|
||||
|
||||
instance, err := p.Parse(urn, "example.test.model")
|
||||
if err != nil {
|
||||
// Init may fail due to no network (std lib refresh), parse may still work
|
||||
t.Logf("Parse result: instance=%v, err=%v", instance != nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MIoTSpecInstance / MIoTSpecService / MIoTSpecProperty
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecInstance_GetServices(t *testing.T) {
|
||||
svc := &miot.MIoTSpecService{IID: 1, Type: "test"}
|
||||
instance := &miot.MIoTSpecInstance{
|
||||
URN: "urn:test:1",
|
||||
Services: []*miot.MIoTSpecService{svc},
|
||||
}
|
||||
|
||||
services := instance.GetServices()
|
||||
if len(services) != 1 {
|
||||
t.Errorf("GetServices len = %d, want 1", len(services))
|
||||
}
|
||||
if services[0].IID != 1 {
|
||||
t.Errorf("service IID = %d, want 1", services[0].IID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecProperty_Access(t *testing.T) {
|
||||
prop := &miot.MIoTSpecProperty{
|
||||
SIID: 1,
|
||||
PIID: 1,
|
||||
Type: "test",
|
||||
Format: "bool",
|
||||
Access: []string{"read", "write", "notify"},
|
||||
}
|
||||
|
||||
if !prop.Readable() {
|
||||
t.Error("Readable should be true with 'read' in access")
|
||||
}
|
||||
if !prop.Writable() {
|
||||
t.Error("Writable should be true with 'write' in access")
|
||||
}
|
||||
if !prop.Notifiable() {
|
||||
t.Error("Notifiable should be true with 'notify' in access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecProperty_AccessReadOnly(t *testing.T) {
|
||||
prop := &miot.MIoTSpecProperty{
|
||||
SIID: 2,
|
||||
PIID: 3,
|
||||
Type: "test",
|
||||
Format: "float",
|
||||
Access: []string{"read", "notify"},
|
||||
}
|
||||
|
||||
if !prop.Readable() {
|
||||
t.Error("Readable should be true")
|
||||
}
|
||||
if prop.Writable() {
|
||||
t.Error("Writable should be false without 'write'")
|
||||
}
|
||||
if !prop.Notifiable() {
|
||||
t.Error("Notifiable should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecProperty_AccessEmpty(t *testing.T) {
|
||||
prop := &miot.MIoTSpecProperty{
|
||||
SIID: 1,
|
||||
PIID: 1,
|
||||
Access: []string{},
|
||||
}
|
||||
|
||||
if prop.Readable() {
|
||||
t.Error("Readable should be false with empty access")
|
||||
}
|
||||
if prop.Writable() {
|
||||
t.Error("Writable should be false with empty access")
|
||||
}
|
||||
if prop.Notifiable() {
|
||||
t.Error("Notifiable should be false with empty access")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// convertStringMapMap
|
||||
// ============================================================================
|
||||
|
||||
func TestSpec_convertStringMapMap_Implicit(t *testing.T) {
|
||||
// Verify that Load handles nested maps correctly
|
||||
lib := miot.NewSpecStdLib("en")
|
||||
lib.Load(map[string]interface{}{
|
||||
"devices": map[string]interface{}{
|
||||
"k1": map[string]interface{}{
|
||||
"en": "v1",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result := lib.DeviceTranslate("k1")
|
||||
if result != "v1" {
|
||||
t.Errorf("nested map translate = %q, want 'v1'", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Values / ValueRange
|
||||
// ============================================================================
|
||||
|
||||
func TestSpecValueList(t *testing.T) {
|
||||
vl := miot.MIoTSpecValueList{
|
||||
{Value: "a", Desc: "Option A"},
|
||||
{Value: "b", Desc: "Option B"},
|
||||
}
|
||||
|
||||
if len(vl) != 2 {
|
||||
t.Errorf("value list len = %d, want 2", len(vl))
|
||||
}
|
||||
if vl[0].Value != "a" || vl[0].Desc != "Option A" {
|
||||
t.Errorf("value list entry = %v/%v", vl[0].Value, vl[0].Desc)
|
||||
}
|
||||
|
||||
// Test methods on the ValueList slice type
|
||||
names := vl.Names()
|
||||
if len(names) != 2 {
|
||||
t.Errorf("Names() len = %d, want 2", len(names))
|
||||
}
|
||||
|
||||
values := vl.Values()
|
||||
if len(values) != 2 {
|
||||
t.Errorf("Values() len = %d, want 2", len(values))
|
||||
}
|
||||
|
||||
descs := vl.Descriptions()
|
||||
if len(descs) != 2 {
|
||||
t.Errorf("Descriptions() len = %d, want 2", len(descs))
|
||||
}
|
||||
if descs[0] != "Option A" {
|
||||
t.Errorf("Descriptions()[0] = %q, want 'Option A'", descs[0])
|
||||
}
|
||||
|
||||
// GetValueByDescription
|
||||
val := vl.GetValueByDescription("Option B")
|
||||
if val != "b" {
|
||||
t.Errorf("GetValueByDescription('Option B') = %v, want 'b'", val)
|
||||
}
|
||||
|
||||
// GetDescriptionByValue
|
||||
desc := vl.GetDescriptionByValue("a")
|
||||
if desc != "Option A" {
|
||||
t.Errorf("GetDescriptionByValue('a') = %q, want 'Option A'", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecValueRange(t *testing.T) {
|
||||
vr := &miot.MIoTSpecValueRange{
|
||||
Min: float64(0),
|
||||
Max: float64(100),
|
||||
Step: float64(1),
|
||||
}
|
||||
|
||||
if vr.Min != float64(0) {
|
||||
t.Errorf("Min = %v", vr.Min)
|
||||
}
|
||||
if vr.Max != float64(100) {
|
||||
t.Errorf("Max = %v", vr.Max)
|
||||
}
|
||||
if vr.Step != float64(1) {
|
||||
t.Errorf("Step = %v", vr.Step)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user