feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
// Package miot_test provides black-box tests for common.go utility functions.
|
||||
package miot_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
miot "miot"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// CalcGroupID
|
||||
// ============================================================================
|
||||
|
||||
func TestCalcGroupID(t *testing.T) {
|
||||
uid := "test_user"
|
||||
homeID := "12345"
|
||||
|
||||
id1 := miot.CalcGroupID(uid, homeID)
|
||||
id2 := miot.CalcGroupID(uid, homeID)
|
||||
|
||||
// Same inputs → same output
|
||||
if id1 != id2 {
|
||||
t.Errorf("CalcGroupID should be deterministic: %q != %q", id1, id2)
|
||||
}
|
||||
|
||||
// Length should be 16 (hex encoding of first 8 bytes of SHA1)
|
||||
if len(id1) != 16 {
|
||||
t.Errorf("CalcGroupID length = %d, want 16", len(id1))
|
||||
}
|
||||
|
||||
// Should be valid hex
|
||||
if _, err := hex.DecodeString(id1); err != nil {
|
||||
t.Errorf("CalcGroupID should produce valid hex: %v", err)
|
||||
}
|
||||
|
||||
// Different inputs → different output
|
||||
id3 := miot.CalcGroupID(uid, "99999")
|
||||
if id1 == id3 {
|
||||
t.Error("CalcGroupID should produce different results for different inputs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcGroupID_EmptyInputs(t *testing.T) {
|
||||
// Empty inputs should still work without panic
|
||||
id := miot.CalcGroupID("", "")
|
||||
if len(id) != 16 {
|
||||
t.Errorf("CalcGroupID with empty inputs length = %d, want 16", len(id))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GenAbsolutePath
|
||||
// ============================================================================
|
||||
|
||||
func TestGenAbsolutePath(t *testing.T) {
|
||||
// Save and restore RootPath
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = "/test/root"
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
abs := miot.GenAbsolutePath("subdir/file.txt")
|
||||
expected := filepath.Join("/test/root", "subdir/file.txt")
|
||||
if abs != expected {
|
||||
t.Errorf("GenAbsolutePath = %q, want %q", abs, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenAbsolutePath_Empty(t *testing.T) {
|
||||
origRoot := miot.RootPath
|
||||
miot.RootPath = "/test/root"
|
||||
defer func() { miot.RootPath = origRoot }()
|
||||
|
||||
abs := miot.GenAbsolutePath("")
|
||||
// Should return RootPath itself when empty string is given
|
||||
if abs != "/test/root" && abs != "/test/root"+string(filepath.Separator) {
|
||||
// filepath.Join behavior varies, just check it's non-empty
|
||||
if abs == "" {
|
||||
t.Error("GenAbsolutePath with empty string should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LoadJSONFile
|
||||
// ============================================================================
|
||||
|
||||
func TestLoadJSONFile_Valid(t *testing.T) {
|
||||
// Create a temporary JSON file
|
||||
tmpDir := t.TempDir()
|
||||
jsonPath := filepath.Join(tmpDir, "test.json")
|
||||
content := `{"key1": "value1", "key2": 42, "key3": true}`
|
||||
if err := os.WriteFile(jsonPath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := miot.LoadJSONFile(jsonPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadJSONFile should succeed: %v", err)
|
||||
}
|
||||
if data["key1"] != "value1" {
|
||||
t.Errorf("key1 = %v, want 'value1'", data["key1"])
|
||||
}
|
||||
if data["key2"].(float64) != 42 {
|
||||
t.Errorf("key2 = %v, want 42", data["key2"])
|
||||
}
|
||||
if data["key3"] != true {
|
||||
t.Errorf("key3 = %v, want true", data["key3"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadJSONFile_NotExist(t *testing.T) {
|
||||
_, err := miot.LoadJSONFile("/nonexistent/path/file.json")
|
||||
if err == nil {
|
||||
t.Error("LoadJSONFile should return error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadJSONFile_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonPath := filepath.Join(tmpDir, "invalid.json")
|
||||
content := `{not valid json`
|
||||
if err := os.WriteFile(jsonPath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := miot.LoadJSONFile(jsonPath)
|
||||
if err == nil {
|
||||
t.Error("LoadJSONFile should return error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadJSONFile_EmptyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonPath := filepath.Join(tmpDir, "empty.json")
|
||||
if err := os.WriteFile(jsonPath, []byte(""), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Empty file is not valid JSON object
|
||||
_, err := miot.LoadJSONFile(jsonPath)
|
||||
if err == nil {
|
||||
t.Error("LoadJSONFile should return error for empty file")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LoadYAMLFile
|
||||
// ============================================================================
|
||||
|
||||
func TestLoadYAMLFile_Valid(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
yamlPath := filepath.Join(tmpDir, "test.yaml")
|
||||
content := "key1: value1\nkey2: 42\nkey3: true\n"
|
||||
if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := miot.LoadYAMLFile(yamlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadYAMLFile should succeed: %v", err)
|
||||
}
|
||||
if data["key1"] != "value1" {
|
||||
t.Errorf("key1 = %v, want 'value1'", data["key1"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadYAMLFile_NotExist(t *testing.T) {
|
||||
_, err := miot.LoadYAMLFile("/nonexistent/path/file.yaml")
|
||||
if err == nil {
|
||||
t.Error("LoadYAMLFile should return error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadYAMLFile_InvalidYAML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
yamlPath := filepath.Join(tmpDir, "invalid.yaml")
|
||||
// Invalid YAML that will cause parsing error
|
||||
content := "\tinvalid\tyaml\tbad"
|
||||
if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// YAML is lenient; this might parse as a string. Just ensure no panic.
|
||||
_, _ = miot.LoadYAMLFile(yamlPath)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RandomizeInt
|
||||
// ============================================================================
|
||||
|
||||
func TestRandomizeInt(t *testing.T) {
|
||||
value := 100
|
||||
ratio := 0.3
|
||||
|
||||
minVal := int(float64(value) * (1 - ratio))
|
||||
maxVal := int(float64(value) * (1 + ratio))
|
||||
|
||||
// Run multiple times to cover the range
|
||||
for i := 0; i < 100; i++ {
|
||||
result := miot.RandomizeInt(value, ratio)
|
||||
if result < minVal || result > maxVal {
|
||||
t.Errorf("RandomizeInt(%d, %f) = %d, expected in [%d, %d]", value, ratio, result, minVal, maxVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomizeInt_ZeroRatio(t *testing.T) {
|
||||
value := 50
|
||||
result := miot.RandomizeInt(value, 0)
|
||||
if result != value {
|
||||
t.Errorf("RandomizeInt(%d, 0) = %d, want %d", value, result, value)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RandomizeFloat
|
||||
// ============================================================================
|
||||
|
||||
func TestRandomizeFloat(t *testing.T) {
|
||||
value := 10.0
|
||||
ratio := 0.2
|
||||
|
||||
minVal := value * (1 - ratio)
|
||||
maxVal := value * (1 + ratio)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
result := miot.RandomizeFloat(value, ratio)
|
||||
if result < minVal || result > maxVal {
|
||||
t.Errorf("RandomizeFloat(%f, %f) = %f, expected in [%f, %f]", value, ratio, result, minVal, maxVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomizeFloat_ZeroRatio(t *testing.T) {
|
||||
value := 3.14
|
||||
result := miot.RandomizeFloat(value, 0)
|
||||
if result != value {
|
||||
t.Errorf("RandomizeFloat(%f, 0) = %f, want %f", value, result, value)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SlugifyName
|
||||
// ============================================================================
|
||||
|
||||
func TestSlugifyName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"Hello World", "hello_world"},
|
||||
{"Xiaomi Home", "xiaomi_home"},
|
||||
{"UPPERCASE", "uppercase"},
|
||||
{"a b c", "a_b_c"},
|
||||
{"already_slug", "already_slug"},
|
||||
{"", ""},
|
||||
{" spaces ", "spaces"},
|
||||
{"special!@#chars", "special_chars"},
|
||||
{"混合中文English", "english"}, // non-alpha chars become underscores, trim
|
||||
{"dash-separated", "dash_separated"}, // dash is non-alphanumeric
|
||||
{"123numbers456", "123numbers456"},
|
||||
{"__leading_trailing__", "leading_trailing"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
if got := miot.SlugifyName(tt.input); got != tt.want {
|
||||
t.Errorf("SlugifyName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SlugifyDID
|
||||
// ============================================================================
|
||||
|
||||
func TestSlugifyDID(t *testing.T) {
|
||||
server := "cn"
|
||||
did := "device_123"
|
||||
|
||||
result := miot.SlugifyDID(server, did)
|
||||
|
||||
// Should equal SlugifyName("cn_device_123")
|
||||
expected := miot.SlugifyName(server + "_" + did)
|
||||
if result != expected {
|
||||
t.Errorf("SlugifyDID(%q, %q) = %q, want %q", server, did, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlugifyDID_NonASCIIDID(t *testing.T) {
|
||||
// Even with non-ASCII DID, should not panic
|
||||
result := miot.SlugifyDID("cn", "did.test")
|
||||
if result == "" {
|
||||
t.Error("SlugifyDID should not return empty string for valid inputs")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RootPath
|
||||
// ============================================================================
|
||||
|
||||
func TestRootPath_Initialized(t *testing.T) {
|
||||
// RootPath should be set during init()
|
||||
if miot.RootPath == "" {
|
||||
t.Error("RootPath should not be empty after init()")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark
|
||||
// ============================================================================
|
||||
|
||||
func BenchmarkCalcGroupID(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
miot.CalcGroupID("test_uid", "test_home")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSlugifyName(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
miot.SlugifyName("Xiaomi Smart Home Device")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user