From c9a847a3898c208fca1bb019845840ec64aff68c Mon Sep 17 00:00:00 2001 From: blacktop Date: Sun, 3 May 2026 11:33:19 -0600 Subject: [PATCH] fix: support legacy factorPackIds format and tests closes #1185 --- pkg/crashlog/ips.go | 32 ++++++++++-- pkg/crashlog/ips_test.go | 105 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 pkg/crashlog/ips_test.go diff --git a/pkg/crashlog/ips.go b/pkg/crashlog/ips.go index 79ae9a0e8..5ad12288c 100644 --- a/pkg/crashlog/ips.go +++ b/pkg/crashlog/ips.go @@ -329,6 +329,31 @@ type BinaryImage struct { Slide uint64 `json:"slide,omitempty"` } +// FactorPackIDs accepts both the current string-list rollout encoding and the legacy object encoding. +type FactorPackIDs []string + +func (ids *FactorPackIDs) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + *ids = nil + return nil + } + + var values []string + arrayErr := json.Unmarshal(data, &values) + if arrayErr == nil { + *ids = values + return nil + } + + var legacyObject struct{} + if err := json.Unmarshal(data, &legacyObject); err == nil { + *ids = nil + return nil + } + + return arrayErr +} + func (bi *BinaryImage) UnmarshalJSON(b []byte) error { var raw []json.RawMessage if err := json.Unmarshal(b, &raw); err != nil { @@ -696,10 +721,9 @@ type IPSPayload struct { } `json:"legacyInfo"` TrialInfo struct { Rollouts []struct { - RolloutID string `json:"rolloutId,omitempty"` - FactorPackIds struct { - } `json:"factorPackIds"` - DeploymentID int `json:"deploymentId,omitempty"` + RolloutID string `json:"rolloutId,omitempty"` + FactorPackIds FactorPackIDs `json:"factorPackIds"` + DeploymentID int `json:"deploymentId,omitempty"` } `json:"rollouts,omitempty"` Experiments []struct { TreatmentID string `json:"treatmentId,omitempty"` diff --git a/pkg/crashlog/ips_test.go b/pkg/crashlog/ips_test.go new file mode 100644 index 000000000..f8bdfa68b --- /dev/null +++ b/pkg/crashlog/ips_test.go @@ -0,0 +1,105 @@ +package crashlog + +import ( + "encoding/json" + "os" + "path/filepath" + "slices" + "testing" +) + +func TestIPSPayloadTrialInfoFactorPackIdsArray(t *testing.T) { + var payload IPSPayload + data := []byte(`{ + "trialInfo": { + "rollouts": [ + { + "rolloutId": "66d35d7fe4d6bf7664f40ddf", + "factorPackIds": ["68c1a34bd359577bbe8f2182"], + "deploymentId": 240000067 + } + ] + } + }`) + + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + + rollouts := payload.TrialInfo.Rollouts + if len(rollouts) != 1 { + t.Fatalf("expected 1 rollout, got %d", len(rollouts)) + } + assertFactorPackIDs(t, rollouts[0].FactorPackIds, "68c1a34bd359577bbe8f2182") +} + +func TestOpenIPSAcceptsTrialInfoFactorPackIdsArray(t *testing.T) { + path := filepath.Join(t.TempDir(), "ios26.ips") + data := []byte(`{"bug_type":"309","os_version":"iPhone OS 26.0 (23A000)"} +{ + "modelCode": "iPhone17,1", + "trialInfo": { + "rollouts": [ + { + "rolloutId": "66d35d7fe4d6bf7664f40ddf", + "factorPackIds": ["68c1a34bd359577bbe8f2182"], + "deploymentId": 240000067 + } + ] + } +}`) + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatalf("os.WriteFile: %v", err) + } + + ips, err := OpenIPS(path, &Config{}) + if err != nil { + t.Fatalf("OpenIPS: %v", err) + } + + rollouts := ips.Payload.TrialInfo.Rollouts + if len(rollouts) != 1 { + t.Fatalf("expected 1 rollout, got %d", len(rollouts)) + } + assertFactorPackIDs(t, rollouts[0].FactorPackIds, "68c1a34bd359577bbe8f2182") +} + +func TestIPSPayloadTrialInfoFactorPackIdsLegacyObject(t *testing.T) { + var payload IPSPayload + data := []byte(`{ + "trialInfo": { + "rollouts": [ + { + "rolloutId": "66d35d7fe4d6bf7664f40ddf", + "factorPackIds": {}, + "deploymentId": 240000067 + } + ] + } + }`) + + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + + rollouts := payload.TrialInfo.Rollouts + if len(rollouts) != 1 { + t.Fatalf("expected 1 rollout, got %d", len(rollouts)) + } + assertFactorPackIDs(t, rollouts[0].FactorPackIds) +} + +func TestFactorPackIdsRejectsInvalidShape(t *testing.T) { + var factorPackIDs FactorPackIDs + if err := json.Unmarshal([]byte(`"68c1a34bd359577bbe8f2182"`), &factorPackIDs); err == nil { + t.Fatal("expected invalid scalar factorPackIds to fail") + } +} + +func assertFactorPackIDs(t *testing.T, got FactorPackIDs, want ...string) { + t.Helper() + + if !slices.Equal(got, want) { + t.Fatalf("unexpected factor pack IDs: got %#v, want %#v", got, want) + } +}