go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为 "xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目 介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构 设计文档。
328 lines
9.2 KiB
Go
328 lines
9.2 KiB
Go
// Package miot_test provides black-box tests for common.go utility functions.
|
|
package miot_test
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
miot "xiaomihome/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")
|
|
}
|
|
}
|