feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
// Package miot_test provides black-box tests for miot_i18n.go internationalization.
|
||||
package miot_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
miot "miot"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// NewMIoTI18n
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_New(t *testing.T) {
|
||||
i18n := miot.NewMIoTI18n("zh-Hans")
|
||||
if i18n == nil {
|
||||
t.Fatal("NewMIoTI18n returned nil")
|
||||
}
|
||||
// Should not panic on any operation before Init
|
||||
s := i18n.Translate("any.key", nil)
|
||||
if s != "" {
|
||||
t.Errorf("expected empty string before Init, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestI18n_NewEmptyLang(t *testing.T) {
|
||||
// Empty language should still create valid instance
|
||||
i18n := miot.NewMIoTI18n("")
|
||||
if i18n == nil {
|
||||
t.Fatal("NewMIoTI18n with empty lang returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Init / Deinit
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_InitWithValidJSON(t *testing.T) {
|
||||
// Create a temporary i18n JSON file
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
|
||||
jsonContent := `{
|
||||
"device": {
|
||||
"light": {
|
||||
"name": "Light",
|
||||
"description": "Smart Light"
|
||||
},
|
||||
"sensor": {
|
||||
"temperature": "Temperature Sensor"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"on": "On",
|
||||
"off": "Off"
|
||||
}
|
||||
}`
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(jsonContent), 0644)
|
||||
|
||||
// Override RootPath for the test
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// Verify translation
|
||||
result := i18n.Translate("device.light.name", nil)
|
||||
if result != "Light" {
|
||||
t.Errorf("Translate(device.light.name) = %q, want %q", result, "Light")
|
||||
}
|
||||
|
||||
result = i18n.Translate("device.sensor.temperature", nil)
|
||||
if result != "Temperature Sensor" {
|
||||
t.Errorf("Translate = %q, want %q", result, "Temperature Sensor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestI18n_InitTwice(t *testing.T) {
|
||||
// Should not reload if already initialized
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init() // first init
|
||||
i18n.Init() // second init should be no-op
|
||||
// Just verify no panic
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Deinit
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_Deinit(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(`{"greeting": "Hello"}`), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// Before Deinit
|
||||
result := i18n.Translate("greeting", nil)
|
||||
if result != "Hello" {
|
||||
t.Fatalf("Translate before Deinit should work, got %q", result)
|
||||
}
|
||||
|
||||
// After Deinit
|
||||
i18n.Deinit()
|
||||
result = i18n.Translate("greeting", nil)
|
||||
if result != "" {
|
||||
t.Errorf("Translate after Deinit should return empty, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Init with missing file
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_InitMissingFile(t *testing.T) {
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = t.TempDir()
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("fantasy-lang")
|
||||
// Should not panic
|
||||
i18n.Init()
|
||||
|
||||
// Translate should return empty
|
||||
result := i18n.Translate("any.key", nil)
|
||||
if result != "" {
|
||||
t.Errorf("translate with missing file should return empty, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translate — nested keys
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_TranslateNestedKey(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
jsonContent := `{
|
||||
"a": {
|
||||
"b": {
|
||||
"c": "deep_value"
|
||||
}
|
||||
}
|
||||
}`
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(jsonContent), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
result := i18n.Translate("a.b.c", nil)
|
||||
if result != "deep_value" {
|
||||
t.Errorf("nested translate = %q, want 'deep_value'", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translate — missing key
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_TranslateMissingKey(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(`{"hello": "world"}`), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// Missing key
|
||||
result := i18n.Translate("missing.key", nil)
|
||||
if result != "" {
|
||||
t.Errorf("missing key translate = %q, want empty", result)
|
||||
}
|
||||
|
||||
// Partial match
|
||||
result = i18n.Translate("hello.world", nil)
|
||||
if result != "" {
|
||||
t.Errorf("partial match translate = %q, want empty (hello is not a map)", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translate — placeholders
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_TranslateWithReplace(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
jsonContent := `{
|
||||
"message": "Hello {name}, you have {count} messages"
|
||||
}`
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(jsonContent), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
result := i18n.Translate("message", map[string]string{
|
||||
"name": "Alice",
|
||||
"count": "5",
|
||||
})
|
||||
want := "Hello Alice, you have 5 messages"
|
||||
if result != want {
|
||||
t.Errorf("Translate with replace = %q, want %q", result, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestI18n_TranslateWithReplaceNil(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(`{"greet": "Hello {name}"}`), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// replace = nil → no substitution
|
||||
result := i18n.Translate("greet", nil)
|
||||
if result != "Hello {name}" {
|
||||
t.Errorf("Translate with nil replace = %q, want 'Hello {name}'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestI18n_TranslateWithPartialReplace(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(`{"msg": "Hello {name} from {city}"}`), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// Only replace "name", leave "{city}" as-is
|
||||
result := i18n.Translate("msg", map[string]string{"name": "Bob"})
|
||||
if result != "Hello Bob from {city}" {
|
||||
t.Errorf("partial replace = %q, want 'Hello Bob from {city}'", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translate — non-string value
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_TranslateNonStringValue(t *testing.T) {
|
||||
i18nDir := filepath.Join(t.TempDir(), "i18n")
|
||||
os.MkdirAll(i18nDir, 0755)
|
||||
// Value is a nested object (map), not a string
|
||||
jsonContent := `{
|
||||
"config": {
|
||||
"settings": {
|
||||
"timeout": 30
|
||||
}
|
||||
}
|
||||
}`
|
||||
os.WriteFile(filepath.Join(i18nDir, "en.json"), []byte(jsonContent), 0644)
|
||||
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = filepath.Dir(i18nDir)
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
i18n.Init()
|
||||
|
||||
// config.settings is an object, not string → return ""
|
||||
result := i18n.Translate("config.settings", nil)
|
||||
if result != "" {
|
||||
t.Errorf("non-string value should return empty, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translate — empty key
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_TranslateEmptyKey(t *testing.T) {
|
||||
i18n := miot.NewMIoTI18n("en")
|
||||
|
||||
result := i18n.Translate("", nil)
|
||||
if result != "" {
|
||||
t.Errorf("empty key should return empty, got %q", result)
|
||||
}
|
||||
|
||||
result = i18n.Translate("", map[string]string{"x": "y"})
|
||||
if result != "" {
|
||||
t.Errorf("empty key with replace should return empty, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Init to verify i18n JSON files exist on disk
|
||||
// ============================================================================
|
||||
|
||||
func TestI18n_InitFromProjectFiles(t *testing.T) {
|
||||
// This test verifies that actual i18n JSON files exist in the project
|
||||
// and can be loaded. Relies on RootPath being set to workspace.
|
||||
|
||||
// Find the i18n directory relative to test file
|
||||
// The i18n directory is at go-miot/i18n/
|
||||
i18nDir := filepath.Join(miot.RootPath, "i18n")
|
||||
entries, err := os.ReadDir(i18nDir)
|
||||
if err != nil {
|
||||
t.Skipf("i18n directory not found at %s, skipping: %v", i18nDir, err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
t.Skip("i18n directory is empty, skipping")
|
||||
}
|
||||
|
||||
// Pick first JSON file
|
||||
var firstLang string
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) == ".json" {
|
||||
firstLang = e.Name()[:len(e.Name())-5] // strip .json
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstLang == "" {
|
||||
t.Skip("no JSON files in i18n directory, skipping")
|
||||
}
|
||||
|
||||
i18n := miot.NewMIoTI18n(firstLang)
|
||||
i18n.Init()
|
||||
|
||||
// Should have data loaded
|
||||
// Test a few common translation keys
|
||||
commonKeys := []string{
|
||||
"device.switch.on",
|
||||
"device.switch.off",
|
||||
}
|
||||
for _, key := range commonKeys {
|
||||
result := i18n.Translate(key, nil)
|
||||
if result == "" {
|
||||
t.Logf("note: key %q has no translation for %s", key, firstLang)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user