fix: preserve partial batch property results

This commit is contained in:
2026-07-22 11:38:23 +08:00
parent 013056d715
commit a22457ce5f
3 changed files with 63 additions and 9 deletions
+17 -3
View File
@@ -747,6 +747,7 @@ func (device *Device) GetMany(ctx context.Context, names []string) ([]DeviceProp
return nil, err return nil, err
} }
byIdentity := make(map[propertyIdentity]PropertyResult, len(chunkResults)) byIdentity := make(map[propertyIdentity]PropertyResult, len(chunkResults))
duplicateIdentities := make(map[propertyIdentity]struct{})
requested := make(map[propertyIdentity]struct{}, end-start) requested := make(map[propertyIdentity]struct{}, end-start)
for _, request := range requests[start:end] { for _, request := range requests[start:end] {
requested[propertyIdentity{did: request.DID, siid: request.SIID, piid: request.PIID}] = struct{}{} requested[propertyIdentity{did: request.DID, siid: request.SIID, piid: request.PIID}] = struct{}{}
@@ -757,17 +758,30 @@ func (device *Device) GetMany(ctx context.Context, names []string) ([]DeviceProp
return nil, fmt.Errorf("get properties protocol error: unexpected identity (%s,%d,%d)", result.DID, result.SIID, result.PIID) return nil, fmt.Errorf("get properties protocol error: unexpected identity (%s,%d,%d)", result.DID, result.SIID, result.PIID)
} }
if _, duplicate := byIdentity[identity]; duplicate { if _, duplicate := byIdentity[identity]; duplicate {
return nil, fmt.Errorf("get properties protocol error: duplicate identity (%s,%d,%d)", result.DID, result.SIID, result.PIID) duplicateIdentities[identity] = struct{}{}
continue
} }
byIdentity[identity] = result byIdentity[identity] = result
} }
for index, request := range requests[start:end] { for index, request := range requests[start:end] {
identity := propertyIdentity{did: request.DID, siid: request.SIID, piid: request.PIID} identity := propertyIdentity{did: request.DID, siid: request.SIID, piid: request.PIID}
name := names[start+index]
if _, duplicate := duplicateIdentities[identity]; duplicate {
results[start+index] = DevicePropertyResult{
Name: name,
Err: fmt.Errorf("get properties protocol error: duplicate identity (%s,%d,%d)", request.DID, request.SIID, request.PIID),
}
continue
}
result, ok := byIdentity[identity] result, ok := byIdentity[identity]
if !ok { if !ok {
return nil, fmt.Errorf("get properties protocol error: missing identity (%s,%d,%d)", request.DID, request.SIID, request.PIID) results[start+index] = DevicePropertyResult{
Name: name,
Err: fmt.Errorf("get properties protocol error: missing identity (%s,%d,%d)", request.DID, request.SIID, request.PIID),
} }
results[start+index] = DevicePropertyResult{Name: names[start+index], Value: result.Value, Code: result.Code} continue
}
results[start+index] = DevicePropertyResult{Name: name, Value: result.Value, Code: result.Code}
} }
} }
if err := device.wait(ctx); err != nil { if err := device.wait(ctx); err != nil {
+45 -6
View File
@@ -625,26 +625,51 @@ func TestDeviceGetManyMatchesIdentityAndPreservesBusinessErrors(t *testing.T) {
} }
} }
func TestDeviceGetManyRejectsInvalidResponseIdentities(t *testing.T) { func TestDeviceGetManyPreservesPartialResultsForInvalidResponseIdentities(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
response string response string
wantErrs []string
}{ }{
{name: "missing", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0}]`}, {name: "missing", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0}]`, wantErrs: []string{"", "missing identity"}},
{name: "duplicate", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0},{"did":"a","siid":2,"piid":1,"value":false,"code":0}]`}, {name: "duplicate and missing", response: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0},{"did":"a","siid":2,"piid":1,"value":false,"code":0}]`, wantErrs: []string{"duplicate identity", "missing identity"}},
{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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
device := fixtureDeviceWithResults(t, []string{test.response}, 0) device := fixtureDeviceWithResults(t, []string{test.response}, 0)
results, err := device.GetMany(context.Background(), []string{"power", "brightness"}) results, err := device.GetMany(context.Background(), []string{"power", "brightness"})
if err == nil || results != nil || !strings.Contains(err.Error(), "protocol") { if err != nil || len(results) != 2 {
t.Fatalf("GetMany() = %#v, %v, want nil protocol error", results, err) t.Fatalf("GetMany() = %#v, %v", results, err)
}
for index, wantErr := range test.wantErrs {
if results[index].Name != []string{"power", "brightness"}[index] {
t.Fatalf("result %d name = %q", index, results[index].Name)
}
if wantErr == "" {
if results[index].Err != nil || results[index].Value != true {
t.Fatalf("result %d = %#v, want successful power result", index, results[index])
}
} else if results[index].Err == nil || !strings.Contains(results[index].Err.Error(), wantErr) {
t.Fatalf("result %d error = %v, want %q", index, results[index].Err, wantErr)
}
} }
}) })
} }
} }
func TestDeviceGetManyRejectsExtraResponseIdentity(t *testing.T) {
device := fixtureDeviceWithResults(t, []string{`[
{"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}
]`}, 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) { func TestDeviceGetManyValidatesBeforeNetwork(t *testing.T) {
device, testServer := fixtureDeviceWithServer(t, nil) device, testServer := fixtureDeviceWithServer(t, nil)
tests := [][]string{{"power", "power"}, {"power", "missing"}, {"power", "write-only"}} tests := [][]string{{"power", "power"}, {"power", "missing"}, {"power", "write-only"}}
@@ -692,6 +717,20 @@ func TestDeviceGetManyWaitsOnceAfterAllChunks(t *testing.T) {
} }
} }
func TestDeviceGetManyWaitsOnceWithPartialProtocolErrors(t *testing.T) {
device := fixtureDeviceWithResults(t, []string{`[{"did":"a","siid":2,"piid":1,"value":true,"code":0}]`}, 40*time.Millisecond)
started := time.Now()
results, err := device.GetMany(context.Background(), []string{"power", "brightness"})
elapsed := time.Since(started)
if err != nil || len(results) != 2 || results[1].Err == nil {
t.Fatalf("GetMany() = %#v, %v", results, err)
}
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) { func batchPropertyFixture(count int) (map[string]PropertySpec, []string) {
properties := make(map[string]PropertySpec, count) properties := make(map[string]PropertySpec, count)
names := make([]string, count) names := make([]string, count)
+1
View File
@@ -163,6 +163,7 @@ type DevicePropertyResult struct {
Name string Name string
Value any Value any
Code int Code int
Err error `json:"-"`
} }
type ActionRequest struct { type ActionRequest struct {