// 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) } }