feat: batch device property reads

This commit is contained in:
2026-07-22 09:45:16 +08:00
parent 3f05417b83
commit 013056d715
3 changed files with 211 additions and 0 deletions
+134
View File
@@ -569,6 +569,140 @@ func TestDeviceGetSetAndAction(t *testing.T) {
}
}
func TestDeviceGetManyChunksProperties(t *testing.T) {
for _, count := range []int{20, 21} {
t.Run(fmt.Sprint(count), func(t *testing.T) {
properties, names := batchPropertyFixture(count)
responses := make([]string, 0, (count+19)/20)
for start := 0; start < count; start += 20 {
end := min(start+20, count)
items := make([]PropertyResult, 0, end-start)
for index := start; index < end; index++ {
property := properties[names[index]]
items = append(items, PropertyResult{DID: "a", SIID: property.SIID, PIID: property.PIID, Value: index, Code: 0})
}
payload, err := json.Marshal(items)
if err != nil {
t.Fatal(err)
}
responses = append(responses, string(payload))
}
device, testServer := fixtureDeviceWithServer(t, responses)
device.properties = properties
results, err := device.GetMany(context.Background(), names)
if err != nil || len(results) != count {
t.Fatalf("GetMany() = %#v, %v", results, err)
}
wantCalls := (count + 19) / 20
if got := len(testServer.requests) - 2; got != wantCalls {
t.Fatalf("property calls = %d, want %d", got, wantCalls)
}
for index, request := range testServer.requests[2:] {
params := request["params"].([]any)
wantSize := min(20, count-index*20)
if len(params) != wantSize {
t.Fatalf("chunk %d size = %d, want %d", index, len(params), wantSize)
}
}
})
}
}
func TestDeviceGetManyMatchesIdentityAndPreservesBusinessErrors(t *testing.T) {
device := fixtureDeviceWithResults(t, []string{`[
{"did":"a","siid":2,"piid":2,"code":-704030013},
{"did":"a","siid":2,"piid":1,"value":true,"code":0}
]`}, 0)
results, err := device.GetMany(context.Background(), []string{"power", "brightness"})
if err != nil {
t.Fatal(err)
}
want := []DevicePropertyResult{{Name: "power", Value: true, Code: 0}, {Name: "brightness", Code: -704030013}}
if !reflect.DeepEqual(results, want) {
t.Fatalf("GetMany() = %#v, want %#v", results, want)
}
}
func TestDeviceGetManyRejectsInvalidResponseIdentities(t *testing.T) {
tests := []struct {
name string
response string
}{
{name: "missing", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0}]`},
{name: "duplicate", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0},{"did":"a","siid":2,"piid":1,"value":false,"code":0}]`},
{name: "extra", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0},{"did":"a","siid":2,"piid":2,"value":5,"code":0},{"did":"other","siid":9,"piid":9,"value":1,"code":0}]`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
device := fixtureDeviceWithResults(t, []string{test.response}, 0)
results, err := device.GetMany(context.Background(), []string{"power", "brightness"})
if err == nil || results != nil || !strings.Contains(err.Error(), "protocol") {
t.Fatalf("GetMany() = %#v, %v, want nil protocol error", results, err)
}
})
}
}
func TestDeviceGetManyValidatesBeforeNetwork(t *testing.T) {
device, testServer := fixtureDeviceWithServer(t, nil)
tests := [][]string{{"power", "power"}, {"power", "missing"}, {"power", "write-only"}}
for _, names := range tests {
if results, err := device.GetMany(context.Background(), names); err == nil || results != nil {
t.Fatalf("GetMany(%v) = %#v, %v", names, results, err)
}
}
if len(testServer.requests) != 2 {
t.Fatalf("requests = %d, validation reached network", len(testServer.requests))
}
empty, err := device.GetMany(context.Background(), nil)
if err != nil || empty == nil || len(empty) != 0 {
t.Fatalf("GetMany(nil) = %#v, %v", empty, err)
}
}
func TestDeviceGetManyWaitsOnceAfterAllChunks(t *testing.T) {
properties, names := batchPropertyFixture(21)
responses := make([]string, 2)
for chunk := range responses {
start := chunk * 20
end := min(start+20, len(names))
items := make([]PropertyResult, 0, end-start)
for index := start; index < end; index++ {
property := properties[names[index]]
items = append(items, PropertyResult{DID: "a", SIID: property.SIID, PIID: property.PIID, Value: index})
}
payload, err := json.Marshal(items)
if err != nil {
t.Fatal(err)
}
responses[chunk] = string(payload)
}
device := fixtureDeviceWithResults(t, responses, 40*time.Millisecond)
device.properties = properties
started := time.Now()
if _, err := device.GetMany(context.Background(), names); err != nil {
t.Fatal(err)
}
elapsed := time.Since(started)
if elapsed < 30*time.Millisecond || elapsed >= 75*time.Millisecond {
t.Fatalf("GetMany() delay = %v, want one approximately 40ms wait", elapsed)
}
}
func batchPropertyFixture(count int) (map[string]PropertySpec, []string) {
properties := make(map[string]PropertySpec, count)
names := make([]string, count)
for index := range count {
name := fmt.Sprintf("property-%02d", index)
names[index] = name
properties[name] = PropertySpec{Name: name, Type: "int", RW: "r", SIID: 10 + index/10, PIID: index%10 + 1}
}
return properties, names
}
func TestDeviceMetadataSnapshotsSupportConcurrentReads(t *testing.T) {
device := fixtureDevice(t)
var waitGroup sync.WaitGroup