diff --git a/internal/entities/system/system.go b/internal/entities/system/system.go index 9091aea0..1e85b98a 100644 --- a/internal/entities/system/system.go +++ b/internal/entities/system/system.go @@ -44,7 +44,7 @@ type Stats struct { MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"-"` // [sent bytes, recv bytes] // TODO: remove other load fields in future release in favor of load avg array LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"` - Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state] + Battery Battery `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state] NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download] DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes] MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes] @@ -71,6 +71,15 @@ func (s Uint8Slice) MarshalJSON() ([]byte, error) { return json.Marshal(arr) } +// Battery stores the representative battery's percent and charge state. +// Its custom JSON encoding keeps the public and persisted representation as a +// numeric tuple under both encoding/json v1 and v2. +type Battery [2]uint8 + +func (b Battery) MarshalJSON() ([]byte, error) { + return json.Marshal([2]uint16{uint16(b[0]), uint16(b[1])}) +} + type GPUData struct { Name string `json:"n" cbor:"0,keyasint"` Temperature float64 `json:"-"` @@ -155,8 +164,8 @@ type Info struct { LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"` ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"` - Services []uint16 `json:"sv,omitempty" cbor:"22,keyasint,omitempty"` // [totalServices, numFailedServices] - Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state] + Services []uint16 `json:"sv,omitempty" cbor:"22,keyasint,omitempty"` // [totalServices, numFailedServices] + Battery Battery `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state] RootDiskName string `json:"rdn,omitempty" cbor:"24,keyasint,omitempty"` // custom name for root disk (set via FILESYSTEM=device__name) } diff --git a/internal/entities/system/system_test.go b/internal/entities/system/system_test.go index c389a806..95c2a904 100644 --- a/internal/entities/system/system_test.go +++ b/internal/entities/system/system_test.go @@ -2,6 +2,7 @@ package system import ( "encoding/json" + jsonv2 "encoding/json/v2" "testing" "github.com/fxamacker/cbor/v2" @@ -12,12 +13,19 @@ import ( func TestStatsBatteryTransport(t *testing.T) { stats := Stats{Battery: [2]uint8{0, 1}, Batteries: map[string]uint8{"Primary": 0, "Mouse": 75}} - jsonData, err := json.Marshal(stats) - require.NoError(t, err) - var jsonPayload map[string]any - require.NoError(t, json.Unmarshal(jsonData, &jsonPayload)) - assert.Equal(t, []any{float64(0), float64(1)}, jsonPayload["bat"]) - assert.Equal(t, map[string]any{"Primary": float64(0), "Mouse": float64(75)}, jsonPayload["bats"]) + for name, marshal := range map[string]func(any) ([]byte, error){ + "json_v1": json.Marshal, + "json_v2": func(value any) ([]byte, error) { return jsonv2.Marshal(value) }, + } { + t.Run(name, func(t *testing.T) { + jsonData, err := marshal(stats) + require.NoError(t, err) + var jsonPayload map[string]any + require.NoError(t, json.Unmarshal(jsonData, &jsonPayload)) + assert.Equal(t, []any{float64(0), float64(1)}, jsonPayload["bat"]) + assert.Equal(t, map[string]any{"Primary": float64(0), "Mouse": float64(75)}, jsonPayload["bats"]) + }) + } cborData, err := cbor.Marshal(stats) require.NoError(t, err) @@ -27,6 +35,12 @@ func TestStatsBatteryTransport(t *testing.T) { assert.Equal(t, stats.Batteries, decoded.Batteries) } +func TestStatsBatteryNumericArrayUnmarshal(t *testing.T) { + var stats Stats + require.NoError(t, json.Unmarshal([]byte(`{"bat":[50,4]}`), &stats)) + assert.Equal(t, Battery{50, 4}, stats.Battery) +} + func TestStatsLegacyBatteryPayload(t *testing.T) { data, err := json.Marshal(Stats{Battery: [2]uint8{50, 4}}) require.NoError(t, err) diff --git a/internal/records/records_averaging_test.go b/internal/records/records_averaging_test.go index c1750e56..bc974499 100644 --- a/internal/records/records_averaging_test.go +++ b/internal/records/records_averaging_test.go @@ -620,7 +620,7 @@ func TestAverageSystemStatsSlice_ZeroRepresentativeBattery(t *testing.T) { {Battery: [2]uint8{0, 1}, Batteries: map[string]uint8{"Primary": 0}}, {}, }) - assert.Equal(t, [2]uint8{0, 1}, result.Battery) + assert.Equal(t, system.Battery{0, 1}, result.Battery) assert.Equal(t, map[string]uint8{"Primary": 0}, result.Batteries) }