Files
xiaomihome/miot/spec_parser_test.go
T
4566704 cdc459b5ff fix(xiaomihome): 模块路径改为 git.zeroonesoft.cn/golib/xiaomihome
- 改动:go.mod module 声明与全部内部导入路径补全为完整域名路径,README 同步
- 原因:裸模块名无法被 go get 解析,发版前必须修正为 GOPRIVATE 可拉取路径
2026-10-05 08:53:23 +08:00

548 lines
14 KiB
Go

// Package miot_test provides black-box tests for spec_parser.go SPEC parsing.
package miot_test
import (
"os"
"path/filepath"
"testing"
miot "git.zeroonesoft.cn/golib/xiaomihome/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)
}
}