mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Merge 44859a54f3 into 7e7aadc207
This commit is contained in:
commit
9152cda87c
6 changed files with 2446 additions and 1218 deletions
2869
coverage.txt
2869
coverage.txt
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
|
|
@ -17,6 +18,22 @@ import (
|
|||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var PATHS_TO_CONVERT_BIGMETRICS = []map[string]string{
|
||||
{"ClientStats.cpu_stats.cpu_usage.total_usage": "nanoseconds"},
|
||||
{"ClientStats.cpu_stats.cpu_usage.percpu_usage": "nanoseconds"},
|
||||
{"ClientStats.cpu_stats.cpu_usage.usage_in_kernelmode": "nanoseconds"},
|
||||
{"ClientStats.cpu_stats.cpu_usage.usage_in_usermode": "nanoseconds"},
|
||||
{"ClientStats.cpu_stats.system_cpu_usage": "nanoseconds"},
|
||||
{"ClientStats.precpu_stats.cpu_usage.total_usage": "nanoseconds"},
|
||||
{"ClientStats.precpu_stats.cpu_usage.percpu_usage": "nanoseconds"},
|
||||
{"ClientStats.precpu_stats.cpu_usage.usage_in_kernelmode": "nanoseconds"},
|
||||
{"ClientStats.precpu_stats.cpu_usage.usage_in_usermode": "nanoseconds"},
|
||||
{"ClientStats.precpu_stats.system_cpu_usage": "nanoseconds"},
|
||||
{"ClientStats.memory_stats.limit": "bytes"},
|
||||
{"ClientStats.memory_stats.stats.hierarchical_memory_limit": "bytes"},
|
||||
{"ClientStats.memory_stats.stats.hierarchical_memsw_limit": "bytes"},
|
||||
}
|
||||
|
||||
func RenderStats(userConfig *config.UserConfig, container *commands.Container, viewWidth int) (string, error) {
|
||||
stats, ok := container.GetLastStats()
|
||||
if !ok {
|
||||
|
|
@ -37,7 +54,23 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
|
|||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||
|
||||
originalStats, err := utils.MarshalIntoYaml(stats)
|
||||
statsJsonBytes, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var statsMap map[string]interface{}
|
||||
err = json.Unmarshal(statsJsonBytes, &statsMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = convertBigMetric(&statsMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
originalStats, err := utils.MarshalIntoYaml(statsMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -51,6 +84,7 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
|
|||
)
|
||||
|
||||
return contents, nil
|
||||
|
||||
}
|
||||
|
||||
// plotGraph returns the plotted graph based on the graph spec and the stat history
|
||||
|
|
@ -144,3 +178,78 @@ func getFloat(unk interface{}) (float64, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convertBigMetric(data *map[string]interface{}) error {
|
||||
// switch value := schema.(type) {
|
||||
// case map[string]interface{}:
|
||||
// for key, val := range value {
|
||||
// path = fmt.Sprintf("%s.%s", path, key)
|
||||
// if err := convertBigMetricFromSchema(data, path, val); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// case []string:
|
||||
// // use path to translate from []int to []string
|
||||
// fmt.Println("Converting []int64 to []string for path:", path)
|
||||
// metric, err := lookup.LookupString(data, strings.TrimPrefix(path, "."))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if reflect.TypeOf(metric.Interface()) != reflect.TypeOf([]int64{}) {
|
||||
// return fmt.Errorf("Can't convert non []int64 %v to []string", reflect.TypeOf(metric.Interface()))
|
||||
// } else {
|
||||
// longIntSlice := metric.Interface().([]int64)
|
||||
// formattedMetric := lo.Map(longIntSlice, func(val int64, index int) string {
|
||||
// return utils.FormatBigMetric(val, value[index])
|
||||
// })
|
||||
// return utils.SetObjectFieldByPath(data, path, formattedMetric)
|
||||
// }
|
||||
// case string:
|
||||
// // use path to translate from int/int64 to string
|
||||
// fmt.Println("Converting int64 to string for path:", path)
|
||||
// metric, err := lookup.LookupString(data, strings.TrimPrefix(path, "."))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if reflect.TypeOf(metric.Interface()) != reflect.TypeOf(int64(0)) {
|
||||
// return fmt.Errorf("Can't convert non int64 %v to string", reflect.TypeOf(metric.Interface()))
|
||||
// } else {
|
||||
// formattedMetric := utils.FormatBigMetric(metric.Interface().(int64), value)
|
||||
// return utils.SetObjectFieldByPath(data, path, formattedMetric)
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
for _, pathToConvert := range PATHS_TO_CONVERT_BIGMETRICS {
|
||||
for path, metricType := range pathToConvert {
|
||||
metric, err := lookup.LookupString(data, path)
|
||||
if err != nil {
|
||||
if err == lookup.ErrKeyNotFound {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !metric.IsValid() {
|
||||
continue
|
||||
}
|
||||
if reflect.TypeOf(metric.Interface()) == reflect.TypeOf(float64(0)) {
|
||||
formattedMetric := utils.FormatBigMetric(int64(metric.Interface().(float64)), metricType)
|
||||
err = utils.SetObjectFieldByPath(data, fmt.Sprintf(".%s", path), formattedMetric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if reflect.TypeOf(metric.Interface()) == reflect.TypeOf([]float64{}) {
|
||||
longIntSlice := lo.Map(metric.Interface().([]float64), func(val float64, index int) int64 {
|
||||
return int64(val)
|
||||
})
|
||||
formattedMetric := lo.Map(longIntSlice, func(val int64, index int) string {
|
||||
return utils.FormatBigMetric(val, metricType)
|
||||
})
|
||||
err = utils.SetObjectFieldByPath(data, fmt.Sprintf(".%s", path), formattedMetric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
224
pkg/gui/presentation/container_stats_test.go
Normal file
224
pkg/gui/presentation/container_stats_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertBigMetricFromSchema(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
data map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
expectedErr string
|
||||
}
|
||||
|
||||
inJSONBytes, err := os.ReadFile("testdata/stats.json")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read testdata/stats.json: %v", err)
|
||||
}
|
||||
loadInJSON := func() map[string]interface{} {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(inJSONBytes, &m); err != nil {
|
||||
t.Fatalf("failed to unmarshal testdata/stats.json: %v", err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
setNested := func(m map[string]interface{}, keys []string, val interface{}) {
|
||||
for _, k := range keys[:len(keys)-1] {
|
||||
m = m[k].(map[string]interface{})
|
||||
}
|
||||
m[keys[len(keys)-1]] = val
|
||||
}
|
||||
|
||||
inJSONExpected := loadInJSON()
|
||||
setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "limit"}, "1.911GB")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "stats", "hierarchical_memory_limit"}, "0")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "stats", "hierarchical_memsw_limit"}, "0")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "total_usage"}, "30.283ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "usage_in_kernelmode"}, "15.664ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "usage_in_usermode"}, "14.619ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "system_cpu_usage"}, "25.994h")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "total_usage"}, "30.283ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "usage_in_kernelmode"}, "15.664ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "usage_in_usermode"}, "14.619ms")
|
||||
setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "system_cpu_usage"}, "25.994h")
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
name: "string schema: converts float64 bytes",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": float64(2048),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": "2.000KB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string schema: converts int64 nanoseconds",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"cpu_stats": map[string]interface{}{
|
||||
"cpu_usage": map[string]interface{}{
|
||||
"total_usage": float64(100000000000),
|
||||
"percpu_usage": []float64{50000000000, 50000000000},
|
||||
"usage_in_kernelmode": float64(200000),
|
||||
"usage_in_usermode": float64(200000),
|
||||
},
|
||||
"system_cpu_usage": float64(100000000000),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"cpu_stats": map[string]interface{}{
|
||||
"cpu_usage": map[string]interface{}{
|
||||
"total_usage": "100.000s",
|
||||
"percpu_usage": []string{"50.000s", "50.000s"},
|
||||
"usage_in_kernelmode": "200.000µs",
|
||||
"usage_in_usermode": "200.000µs",
|
||||
},
|
||||
"system_cpu_usage": "100.000s",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty data is a no-op",
|
||||
data: map[string]interface{}{},
|
||||
expected: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "bytes below unit threshold stays as raw number",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": float64(500),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": "500",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bytes at MB scale",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": float64(1500000),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": "1.431MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts deeply nested hierarchical memory limits",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"stats": map[string]interface{}{
|
||||
"hierarchical_memory_limit": float64(1073741824),
|
||||
"hierarchical_memsw_limit": float64(2147483648),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"stats": map[string]interface{}{
|
||||
"hierarchical_memory_limit": "1.000GB",
|
||||
"hierarchical_memsw_limit": "2.000GB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts precpu_stats branch",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"precpu_stats": map[string]interface{}{
|
||||
"cpu_usage": map[string]interface{}{
|
||||
"total_usage": float64(1500000),
|
||||
"percpu_usage": []float64{1000, 2000000000},
|
||||
},
|
||||
"system_cpu_usage": float64(1500000000000),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"precpu_stats": map[string]interface{}{
|
||||
"cpu_usage": map[string]interface{}{
|
||||
"total_usage": "1.500ms",
|
||||
"percpu_usage": []string{"1.000µs", "2.000s"},
|
||||
},
|
||||
"system_cpu_usage": "1.500m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preserves fields not listed in PATHS_TO_CONVERT_BIGMETRICS",
|
||||
data: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": float64(2048),
|
||||
"usage": float64(1024),
|
||||
},
|
||||
"name": "my-container",
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ClientStats": map[string]interface{}{
|
||||
"memory_stats": map[string]interface{}{
|
||||
"limit": "2.000KB",
|
||||
"usage": float64(1024),
|
||||
},
|
||||
"name": "my-container",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts realistic docker stats loaded from testdata/stats.json",
|
||||
data: loadInJSON(),
|
||||
expected: inJSONExpected,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
data := s.data
|
||||
err := convertBigMetric(&data)
|
||||
if s.expectedErr != "" {
|
||||
assert.EqualError(t, err, s.expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, s.expected, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
122
pkg/gui/presentation/testdata/stats.json
vendored
Normal file
122
pkg/gui/presentation/testdata/stats.json
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"ClientStats": {
|
||||
"blkio_stats": {
|
||||
"io_merged_recursive": null,
|
||||
"io_queue_recursive": null,
|
||||
"io_service_bytes_recursive": [
|
||||
{
|
||||
"major": 253,
|
||||
"minor": 0,
|
||||
"op": "read",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"major": 253,
|
||||
"minor": 0,
|
||||
"op": "write",
|
||||
"value": 12288
|
||||
}
|
||||
],
|
||||
"io_service_time_recursive": null,
|
||||
"io_serviced_recursive": null,
|
||||
"io_time_recursive": null,
|
||||
"io_wait_time_recursive": null,
|
||||
"sectors_recursive": null
|
||||
},
|
||||
"cpu_stats": {
|
||||
"cpu_usage": {
|
||||
"percpu_usage": null,
|
||||
"total_usage": 30283000,
|
||||
"usage_in_kernelmode": 15664000,
|
||||
"usage_in_usermode": 14619000
|
||||
},
|
||||
"online_cpus": 2,
|
||||
"system_cpu_usage": 25994230660000000,
|
||||
"throttling_data": {
|
||||
"periods": 0,
|
||||
"throttled_periods": 0,
|
||||
"throttled_time": 0
|
||||
}
|
||||
},
|
||||
"id": "c7dc2bd078294bd06166be7c1cf680cb3ba968a04669b014b2b99666e08c8d57",
|
||||
"memory_stats": {
|
||||
"limit": 2051391488,
|
||||
"max_usage": 0,
|
||||
"stats": {
|
||||
"active_anon": 2695168,
|
||||
"active_file": 0,
|
||||
"cache": 0,
|
||||
"dirty": 0,
|
||||
"hierarchical_memory_limit": 0,
|
||||
"hierarchical_memsw_limit": 0,
|
||||
"inactive_anon": 0,
|
||||
"inactive_file": 69632,
|
||||
"mapped_file": 0,
|
||||
"pgfault": 4656,
|
||||
"pgmajfault": 0,
|
||||
"pgpgin": 0,
|
||||
"pgpgout": 0,
|
||||
"rss": 0,
|
||||
"rss_huge": 0,
|
||||
"total_active_anon": 0,
|
||||
"total_active_file": 0,
|
||||
"total_cache": 0,
|
||||
"total_dirty": 0,
|
||||
"total_inactive_anon": 0,
|
||||
"total_inactive_file": 0,
|
||||
"total_mapped_file": 0,
|
||||
"total_pgfault": 0,
|
||||
"total_pgmajfault": 0,
|
||||
"total_pgpgin": 0,
|
||||
"total_pgpgout": 0,
|
||||
"total_rss": 0,
|
||||
"total_rss_huge": 0,
|
||||
"total_unevictable": 0,
|
||||
"total_writeback": 0,
|
||||
"unevictable": 0,
|
||||
"writeback": 0
|
||||
},
|
||||
"usage": 3465216
|
||||
},
|
||||
"name": "/hungry_margulis",
|
||||
"networks": {
|
||||
"eth0": {
|
||||
"rx_bytes": 112796,
|
||||
"rx_dropped": 0,
|
||||
"rx_errors": 0,
|
||||
"rx_packets": 1610,
|
||||
"tx_bytes": 0,
|
||||
"tx_dropped": 0,
|
||||
"tx_errors": 0,
|
||||
"tx_packets": 0
|
||||
}
|
||||
},
|
||||
"num_procs": 0,
|
||||
"pids_stats": {
|
||||
"current": 3
|
||||
},
|
||||
"precpu_stats": {
|
||||
"cpu_usage": {
|
||||
"percpu_usage": null,
|
||||
"total_usage": 30283000,
|
||||
"usage_in_kernelmode": 15664000,
|
||||
"usage_in_usermode": 14619000
|
||||
},
|
||||
"online_cpus": 2,
|
||||
"system_cpu_usage": 25994228620000000,
|
||||
"throttling_data": {
|
||||
"periods": 0,
|
||||
"throttled_periods": 0,
|
||||
"throttled_time": 0
|
||||
}
|
||||
},
|
||||
"preread": "2026-07-24T21:52:31.132423289Z",
|
||||
"read": "2026-07-24T21:52:32.151677303Z",
|
||||
"storage_stats": {}
|
||||
},
|
||||
"DerivedStats": {
|
||||
"CPUPercentage": 0,
|
||||
"MemoryPercentage": 0.16892026803613217
|
||||
},
|
||||
"RecordedAt": "2026-07-24T22:52:32.092566+01:00"
|
||||
}
|
||||
|
|
@ -410,3 +410,76 @@ func marshalIntoFormat(data interface{}, format string) ([]byte, error) {
|
|||
return nil, errors.New(fmt.Sprintf("Unsupported detailization format: %s", format))
|
||||
}
|
||||
}
|
||||
|
||||
// FormatBigMetric takes a big metric and formats it into a human readable string with the appropriate unit.
|
||||
// For example, if you give it 1000000 with a base unit of bytes, it will return "1.000 MB".
|
||||
// If you give it 1000000000 with a base unit of nanoseconds, it will return "1.000 s"
|
||||
func FormatBigMetric(number int64, baseUnitName string) string {
|
||||
memoryUnits := []string{"KB", "MB", "GB", "TB"}
|
||||
cpuUnits := []string{"µs", "ms", "s", "m", "h"}
|
||||
var unit int64
|
||||
if baseUnitName == "bytes" {
|
||||
unit = 1024
|
||||
} else {
|
||||
unit = 1000
|
||||
}
|
||||
if number < unit {
|
||||
return fmt.Sprintf("%d", number)
|
||||
}
|
||||
div, exp := int64(unit), 1
|
||||
for n := number / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
switch baseUnitName {
|
||||
case "bytes":
|
||||
return fmt.Sprintf("%.3f%s", float64(number)/float64(div), memoryUnits[exp-1])
|
||||
case "nanoseconds":
|
||||
return fmt.Sprintf("%.3f%s", float64(number)/float64(div), cpuUnits[exp-1])
|
||||
default:
|
||||
return fmt.Sprintf("%d", number)
|
||||
}
|
||||
}
|
||||
|
||||
// SetObjectFieldByPath takes an object, a path to a field in that object, and a value,
|
||||
// and sets the field at that path to the value.
|
||||
// For example, if you have an object {"a": {"b": {"c": 1}}}, a path of "a.b.c", and a value of 2,
|
||||
// it will set the object to {"a": {"b": {"c": 2}}}
|
||||
func SetObjectFieldByPath(object *map[string]interface{}, path string, value interface{}) error {
|
||||
re := regexp.MustCompile(`^\.`)
|
||||
if !re.MatchString(path) && path != "" {
|
||||
return fmt.Errorf("Invalid path format %s, path should start with a dot", path)
|
||||
}
|
||||
|
||||
targetObject := map[string]interface{}{}
|
||||
currentObject := *object
|
||||
targetObject = copyMapByPath(currentObject, targetObject, path, value)
|
||||
*object = targetObject
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyMapByPath is a helper function for SetObjectFieldByPath
|
||||
// that recursively copies an object while setting the value at the right path
|
||||
func copyMapByPath(currentObject map[string]interface{}, targetObject map[string]interface{}, path string, value interface{}) map[string]interface{} {
|
||||
if path == "" {
|
||||
return currentObject
|
||||
}
|
||||
keyPathSlice := strings.Split(path, ".")[1:]
|
||||
currentKeys := make([]string, 0, len(currentObject))
|
||||
for k := range currentObject {
|
||||
currentKeys = append(currentKeys, k)
|
||||
}
|
||||
for _, key := range currentKeys {
|
||||
if key != keyPathSlice[0] {
|
||||
targetObject[key] = currentObject[key]
|
||||
} else {
|
||||
if len(keyPathSlice) == 1 {
|
||||
targetObject[key] = value
|
||||
} else {
|
||||
nextObject := currentObject[key].(map[string]interface{})
|
||||
targetObject[key] = copyMapByPath(nextObject, map[string]interface{}{}, "."+strings.Join(keyPathSlice[1:], "."), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetObject
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,3 +319,268 @@ quux:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetObjectFieldByPath(t *testing.T) {
|
||||
type scenario struct {
|
||||
object *map[string]interface{}
|
||||
path string
|
||||
value interface{}
|
||||
expected map[string]interface{}
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": "replacement",
|
||||
},
|
||||
path: ".foo",
|
||||
value: "bar",
|
||||
expected: map[string]interface{}{"foo": "bar"},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "replacement",
|
||||
},
|
||||
},
|
||||
path: ".foo.bar",
|
||||
value: "bar_1",
|
||||
expected: map[string]interface{}{"foo": map[string]interface{}{"bar": "bar_1"}},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"foo_1": "foo_1_1",
|
||||
"foo_2": map[string]interface{}{
|
||||
"foo_2_1": "foo_2_1_1",
|
||||
},
|
||||
"foo_3": map[string]interface{}{
|
||||
"foo_3_1": "foo_3_1_1",
|
||||
"foo_3_2": "foo_3_2_1",
|
||||
"foo_3_3": map[string]interface{}{
|
||||
"foo_3_3_1": "foo_3_3_1_1",
|
||||
"foo_3_3_2": "foo_3_3_2_1",
|
||||
},
|
||||
},
|
||||
"bar": map[string]interface{}{
|
||||
"bar_1": "replacement",
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".foo.bar.bar_1",
|
||||
value: "bar_1_1",
|
||||
expected: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"foo_1": "foo_1_1",
|
||||
"foo_2": map[string]interface{}{
|
||||
"foo_2_1": "foo_2_1_1",
|
||||
},
|
||||
"foo_3": map[string]interface{}{
|
||||
"foo_3_1": "foo_3_1_1",
|
||||
"foo_3_2": "foo_3_2_1",
|
||||
"foo_3_3": map[string]interface{}{
|
||||
"foo_3_3_1": "foo_3_3_1_1",
|
||||
"foo_3_3_2": "foo_3_3_2_1",
|
||||
},
|
||||
},
|
||||
"bar": map[string]interface{}{
|
||||
"bar_1": "bar_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"foo_1": "foo_1_1",
|
||||
"foo_2": map[string]interface{}{
|
||||
"foo_2_1": "foo_2_1_1",
|
||||
},
|
||||
"foo_3": map[string]interface{}{
|
||||
"foo_3_1": "foo_3_1_1",
|
||||
"foo_3_2": "foo_3_2_1",
|
||||
"foo_3_3": map[string]interface{}{
|
||||
"foo_3_3_1": "foo_3_3_1_1",
|
||||
"foo_3_3_2": "foo_3_3_2_1",
|
||||
},
|
||||
},
|
||||
"bar": map[string]interface{}{
|
||||
"bar_1": map[string]interface{}{
|
||||
"bar_1_1": map[string]interface{}{
|
||||
"bar_1_1_1": map[string]interface{}{
|
||||
"bar_1_1_1_1": "replacement",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".foo.bar.bar_1.bar_1_1.bar_1_1_1.bar_1_1_1_1",
|
||||
value: "bar_1_1_1_1_1",
|
||||
expected: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"foo_1": "foo_1_1",
|
||||
"foo_2": map[string]interface{}{
|
||||
"foo_2_1": "foo_2_1_1",
|
||||
},
|
||||
"foo_3": map[string]interface{}{
|
||||
"foo_3_1": "foo_3_1_1",
|
||||
"foo_3_2": "foo_3_2_1",
|
||||
"foo_3_3": map[string]interface{}{
|
||||
"foo_3_3_1": "foo_3_3_1_1",
|
||||
"foo_3_3_2": "foo_3_3_2_1",
|
||||
},
|
||||
},
|
||||
"bar": map[string]interface{}{
|
||||
"bar_1": map[string]interface{}{
|
||||
"bar_1_1": map[string]interface{}{
|
||||
"bar_1_1_1": map[string]interface{}{
|
||||
"bar_1_1_1_1": "bar_1_1_1_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "replacement",
|
||||
},
|
||||
},
|
||||
path: "",
|
||||
value: "bar_1",
|
||||
expected: map[string]interface{}{"foo": map[string]interface{}{"bar": "replacement"}},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
object: &map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "replacement",
|
||||
},
|
||||
},
|
||||
path: "foo.bar",
|
||||
value: "bar_1",
|
||||
expected: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "replacement",
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New("Invalid path format foo.bar, path should start with a dot"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
err := SetObjectFieldByPath(s.object, s.path, s.value)
|
||||
assert.EqualValues(t, s.expected, *s.object)
|
||||
if s.expectedErr != nil {
|
||||
assert.EqualError(t, err, s.expectedErr.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBigMetric(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
number int64
|
||||
baseUnitName string
|
||||
expected string
|
||||
}{
|
||||
// Bytes tests
|
||||
{
|
||||
name: "bytes: small value",
|
||||
number: 500,
|
||||
baseUnitName: "bytes",
|
||||
expected: "500",
|
||||
},
|
||||
{
|
||||
name: "bytes: KB",
|
||||
number: 1500,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1.465KB",
|
||||
},
|
||||
{
|
||||
name: "bytes: MB",
|
||||
number: 1500000,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1.431MB",
|
||||
},
|
||||
{
|
||||
name: "bytes: GB",
|
||||
number: 1500000000,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1.397GB",
|
||||
},
|
||||
{
|
||||
name: "bytes: TB",
|
||||
number: 1500000000000,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1.364TB",
|
||||
},
|
||||
// Nanoseconds tests
|
||||
{
|
||||
name: "nanoseconds: small value",
|
||||
number: 500,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "500",
|
||||
},
|
||||
{
|
||||
name: "nanoseconds: µs",
|
||||
number: 1500,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "1.500µs",
|
||||
},
|
||||
{
|
||||
name: "nanoseconds: ms",
|
||||
number: 1500000,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "1.500ms",
|
||||
},
|
||||
{
|
||||
name: "nanoseconds: s",
|
||||
number: 1500000000,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "1.500s",
|
||||
},
|
||||
{
|
||||
name: "nanoseconds: m",
|
||||
number: 1500000000000,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "1.500m",
|
||||
},
|
||||
{
|
||||
name: "nanoseconds: h",
|
||||
number: 1500000000000000,
|
||||
baseUnitName: "nanoseconds",
|
||||
expected: "1.500h",
|
||||
},
|
||||
// Exact boundaries
|
||||
{
|
||||
name: "bytes: exact 1024",
|
||||
number: 1024,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1.000KB",
|
||||
},
|
||||
{
|
||||
name: "bytes: 1023",
|
||||
number: 1023,
|
||||
baseUnitName: "bytes",
|
||||
expected: "1023",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FormatBigMetric(tt.number, tt.baseUnitName)
|
||||
assert.EqualValues(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue