From 3bd36c3406cc493c66473c6bb30de9321ae1e5b7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 30 Oct 2023 16:45:17 +0100 Subject: [PATCH 1/2] debug: Allow type-specification of JSON output for cortex-debug --- commands/debug/debug_info.go | 41 ++++++++++++++++--- docs/platform-specification.md | 24 +++++++++++ internal/integrationtest/debug/debug_test.go | 18 ++++++++ .../testdata/hardware/my/samd/boards.txt | 6 +++ 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/commands/debug/debug_info.go b/commands/debug/debug_info.go index a8731ab1d6a..d85a766270b 100644 --- a/commands/debug/debug_info.go +++ b/commands/debug/debug_info.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "regexp" + "strconv" "strings" "github.com/arduino/arduino-cli/arduino" @@ -209,28 +210,56 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl // my.indexed.array.2=third // // into the corresponding JSON arrays. +// If a value should be converted into a JSON type different from string, the value +// may be prefiex with "[boolean]", "[number]", or "[object]": +// +// my.stringValue=a string +// my.booleanValue=[boolean]true +// my.numericValue=[number]20 func convertToJsonMap(in *properties.Map) string { - // XXX: Maybe this method could be a good candidate for propertis.Map? - // Find the values that should be kept as is, and the indexed arrays // that should be later converted into arrays. arraysKeys := map[string]bool{} - stringKeys := []string{} + scalarKeys := []string{} trailingNumberMatcher := regexp.MustCompile(`^(.*)\.[0-9]+$`) for _, k := range in.Keys() { match := trailingNumberMatcher.FindAllStringSubmatch(k, -1) if len(match) > 0 && len(match[0]) > 1 { arraysKeys[match[0][1]] = true } else { - stringKeys = append(stringKeys, k) + scalarKeys = append(scalarKeys, k) } } // Compose a map that can be later marshaled into JSON keeping // the arrays where they are expected to be. res := map[string]any{} - for _, k := range stringKeys { - res[k] = in.Get(k) + for _, k := range scalarKeys { + v := in.Get(k) + switch { + case strings.HasPrefix(v, "[boolean]"): + v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]")) + if strings.EqualFold(v, "true") { + res[k] = true + } else if strings.EqualFold(v, "false") { + res[k] = false + } + case strings.HasPrefix(v, "[number]"): + v = strings.TrimPrefix(v, "[number]") + if i, err := strconv.Atoi(v); err == nil { + res[k] = i + } else if f, err := strconv.ParseFloat(v, 64); err == nil { + res[k] = f + } + case strings.HasPrefix(v, "[object]"): + v = strings.TrimPrefix(v, "[object]") + var o interface{} + if err := json.Unmarshal([]byte(v), &o); err == nil { + res[k] = o + } + default: + res[k] = v + } } for k := range arraysKeys { res[k] = in.ExtractSubIndexLists(k) diff --git a/docs/platform-specification.md b/docs/platform-specification.md index c34047bf8d9..ec2f4a3dc3e 100644 --- a/docs/platform-specification.md +++ b/docs/platform-specification.md @@ -1403,6 +1403,30 @@ will result in the following JSON to be merged in the Arduino IDE generated `lau } ``` +All the values are converted by default to a string in the resulting JSON. If another type is needed the value can be +prefixed with the tags `[boolean]`, `[number]`, or `[object]` to force a specific type in the JSON, for example: + +``` +debug.cortex-debug.custom.aBoolean=[boolean]true +debug.cortex-debug.custom.aNumber=[number]10 +debug.cortex-debug.custom.anotherNumber=[number]10.20 +debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true} +``` + +will result in the following JSON: + +```json +{ + "aBoolean": true, + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + } +} +``` + ### Optimization level for debugging The compiler optimization level that is appropriate for normal usage will often not provide a good experience while diff --git a/internal/integrationtest/debug/debug_test.go b/internal/integrationtest/debug/debug_test.go index 0d3a0cc9dbf..4931fa1e9b5 100644 --- a/internal/integrationtest/debug/debug_test.go +++ b/internal/integrationtest/debug/debug_test.go @@ -133,6 +133,15 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli }, "svd_file": "svd-file", "cortex-debug_custom_configuration": { + "aBoolean": true, + "aStringBoolean": "true", + "aStringNumber": "10", + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", @@ -176,6 +185,15 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli }, "svd_file": "svd-file", "cortex-debug_custom_configuration": { + "aBoolean": true, + "aStringBoolean": "true", + "aStringNumber": "10", + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", diff --git a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt index 987aa196fdd..faf869329be 100644 --- a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt +++ b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt @@ -44,6 +44,12 @@ my.debug.cortex-debug.custom.overrideRestartCommands.1=monitor gdb_sync my.debug.cortex-debug.custom.overrideRestartCommands.2=thb setup my.debug.cortex-debug.custom.overrideRestartCommands.3=c my.debug.cortex-debug.custom.anotherStringParamer=hellooo +my.debug.cortex-debug.custom.aBoolean=[boolean]true +my.debug.cortex-debug.custom.aStringBoolean=true +my.debug.cortex-debug.custom.aNumber=[number]10 +my.debug.cortex-debug.custom.anotherNumber=[number]10.20 +my.debug.cortex-debug.custom.aStringNumber=10 +my.debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true} my.debug.svd_file=svd-file my2.name=My Cool Board From 80fec994241ec4ab45b1b544c54f46b057e5212e Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 31 Oct 2023 12:21:58 +0100 Subject: [PATCH 2/2] Improved JSON properties generation --- commands/debug/debug_info.go | 112 +++++++++++------- commands/debug/debug_test.go | 97 +++++++++++++++ docs/platform-specification.md | 9 +- internal/integrationtest/debug/debug_test.go | 8 ++ .../testdata/hardware/my/samd/boards.txt | 2 + 5 files changed, 185 insertions(+), 43 deletions(-) diff --git a/commands/debug/debug_info.go b/commands/debug/debug_info.go index d85a766270b..d7a792953d7 100644 --- a/commands/debug/debug_info.go +++ b/commands/debug/debug_info.go @@ -18,7 +18,7 @@ package debug import ( "context" "encoding/json" - "regexp" + "slices" "strconv" "strings" @@ -217,54 +217,82 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl // my.booleanValue=[boolean]true // my.numericValue=[number]20 func convertToJsonMap(in *properties.Map) string { - // Find the values that should be kept as is, and the indexed arrays - // that should be later converted into arrays. - arraysKeys := map[string]bool{} - scalarKeys := []string{} - trailingNumberMatcher := regexp.MustCompile(`^(.*)\.[0-9]+$`) - for _, k := range in.Keys() { - match := trailingNumberMatcher.FindAllStringSubmatch(k, -1) - if len(match) > 0 && len(match[0]) > 1 { - arraysKeys[match[0][1]] = true - } else { - scalarKeys = append(scalarKeys, k) + data, _ := json.MarshalIndent(convertToRawInterface(in), "", " ") + return string(data) +} + +func allNumerics(in []string) bool { + for _, i := range in { + for _, c := range i { + if c < '0' || c > '9' { + return false + } + } + } + return true +} + +func convertToRawInterface(in *properties.Map) any { + subtrees := in.FirstLevelOf() + keys := in.FirstLevelKeys() + + if allNumerics(keys) { + // Compose an array + res := []any{} + slices.SortFunc(keys, func(x, y string) int { + nx, _ := strconv.Atoi(x) + ny, _ := strconv.Atoi(y) + return nx - ny + }) + for _, k := range keys { + switch { + case subtrees[k] != nil: + res = append(res, convertToRawInterface(subtrees[k])) + default: + res = append(res, convertToRawValue(in.Get(k))) + } } + return res } - // Compose a map that can be later marshaled into JSON keeping - // the arrays where they are expected to be. + // Compose an object res := map[string]any{} - for _, k := range scalarKeys { - v := in.Get(k) + for _, k := range keys { switch { - case strings.HasPrefix(v, "[boolean]"): - v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]")) - if strings.EqualFold(v, "true") { - res[k] = true - } else if strings.EqualFold(v, "false") { - res[k] = false - } - case strings.HasPrefix(v, "[number]"): - v = strings.TrimPrefix(v, "[number]") - if i, err := strconv.Atoi(v); err == nil { - res[k] = i - } else if f, err := strconv.ParseFloat(v, 64); err == nil { - res[k] = f - } - case strings.HasPrefix(v, "[object]"): - v = strings.TrimPrefix(v, "[object]") - var o interface{} - if err := json.Unmarshal([]byte(v), &o); err == nil { - res[k] = o - } + case subtrees[k] != nil: + res[k] = convertToRawInterface(subtrees[k]) default: - res[k] = v + res[k] = convertToRawValue(in.Get(k)) } } - for k := range arraysKeys { - res[k] = in.ExtractSubIndexLists(k) - } + return res +} - data, _ := json.MarshalIndent(res, "", " ") - return string(data) +func convertToRawValue(v string) any { + switch { + case strings.HasPrefix(v, "[boolean]"): + v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]")) + if strings.EqualFold(v, "true") { + return true + } else if strings.EqualFold(v, "false") { + return false + } + case strings.HasPrefix(v, "[number]"): + v = strings.TrimPrefix(v, "[number]") + if i, err := strconv.Atoi(v); err == nil { + return i + } else if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + case strings.HasPrefix(v, "[object]"): + v = strings.TrimPrefix(v, "[object]") + var o interface{} + if err := json.Unmarshal([]byte(v), &o); err == nil { + return o + } + case strings.HasPrefix(v, "[string]"): + v = strings.TrimPrefix(v, "[string]") + } + // default or conversion error, return string as is + return v } diff --git a/commands/debug/debug_test.go b/commands/debug/debug_test.go index 0b99a105c64..3adf4fbfee6 100644 --- a/commands/debug/debug_test.go +++ b/commands/debug/debug_test.go @@ -24,6 +24,7 @@ import ( "github.com/arduino/arduino-cli/arduino/cores/packagemanager" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" "github.com/arduino/go-paths-helper" + "github.com/arduino/go-properties-orderedmap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -90,3 +91,99 @@ func TestGetCommandLine(t *testing.T) { commandToTest2 := strings.Join(command2, " ") assert.Equal(t, filepath.FromSlash(goldCommand2), filepath.FromSlash(commandToTest2)) } + +func TestConvertToJSONMap(t *testing.T) { + testIn := properties.NewFromHashmap(map[string]string{ + "k": "v", + "string": "[string]aaa", + "bool": "[boolean]true", + "number": "[number]10", + "number2": "[number]10.2", + "object": `[object]{ "key":"value", "bool":true }`, + "array.0": "first", + "array.1": "second", + "array.2": "[boolean]true", + "array.3": "[number]10", + "array.4": `[object]{ "key":"value", "bool":true }`, + "array.5.k": "v", + "array.5.bool": "[boolean]true", + "array.5.number": "[number]10", + "array.5.number2": "[number]10.2", + "array.5.object": `[object]{ "key":"value", "bool":true }`, + "array.6.sub.k": "v", + "array.6.sub.bool": "[boolean]true", + "array.6.sub.number": "[number]10", + "array.6.sub.number2": "[number]10.2", + "array.6.sub.object": `[object]{ "key":"value", "bool":true }`, + "array.7.0": "v", + "array.7.1": "[boolean]true", + "array.7.2": "[number]10", + "array.7.3": "[number]10.2", + "array.7.4": `[object]{ "key":"value", "bool":true }`, + "array.8.array.0": "v", + "array.8.array.1": "[boolean]true", + "array.8.array.2": "[number]10", + "array.8.array.3": "[number]10.2", + "array.8.array.4": `[object]{ "key":"value", "bool":true }`, + "sub.k": "v", + "sub.bool": "[boolean]true", + "sub.number": "[number]10", + "sub.number2": "[number]10.2", + "sub.object": `[object]{ "key":"value", "bool":true }`, + }) + jsonString := convertToJsonMap(testIn) + require.JSONEq(t, `{ + "k": "v", + "string": "aaa", + "bool": true, + "number": 10, + "number2": 10.2, + "object": { "key":"value", "bool":true }, + "array": [ + "first", + "second", + true, + 10, + { "key":"value", "bool":true }, + { + "k": "v", + "bool": true, + "number": 10, + "number2": 10.2, + "object": { "key":"value", "bool":true } + }, + { + "sub": { + "k": "v", + "bool": true, + "number": 10, + "number2": 10.2, + "object": { "key":"value", "bool":true } + } + }, + [ + "v", + true, + 10, + 10.2, + { "key":"value", "bool":true } + ], + { + "array": [ + "v", + true, + 10, + 10.2, + { "key":"value", "bool":true } + ] + } + ], + "sub": { + "k": "v", + "bool": true, + "number": 10, + "number2": 10.2, + "object": { "key":"value", "bool":true } + } + }`, jsonString) +} diff --git a/docs/platform-specification.md b/docs/platform-specification.md index ec2f4a3dc3e..83fbf4279ff 100644 --- a/docs/platform-specification.md +++ b/docs/platform-specification.md @@ -1404,13 +1404,16 @@ will result in the following JSON to be merged in the Arduino IDE generated `lau ``` All the values are converted by default to a string in the resulting JSON. If another type is needed the value can be -prefixed with the tags `[boolean]`, `[number]`, or `[object]` to force a specific type in the JSON, for example: +prefixed with the tags `[boolean]`, `[number]`, `[string]` or `[object]` to force a specific type in the JSON. Moreover +the hierarchy of the properties may be used to build JSON objects. For example: ``` debug.cortex-debug.custom.aBoolean=[boolean]true debug.cortex-debug.custom.aNumber=[number]10 debug.cortex-debug.custom.anotherNumber=[number]10.20 debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true} +debug.cortex-debug.custom.anotherObject.key=value +debug.cortex-debug.custom.anotherObject.boolean=[boolean]true ``` will result in the following JSON: @@ -1423,6 +1426,10 @@ will result in the following JSON: "anObject": { "boolean": true, "key": "value" + }, + "anotherObject": { + "boolean": true, + "key": "value" } } ``` diff --git a/internal/integrationtest/debug/debug_test.go b/internal/integrationtest/debug/debug_test.go index 4931fa1e9b5..8c547ba04fa 100644 --- a/internal/integrationtest/debug/debug_test.go +++ b/internal/integrationtest/debug/debug_test.go @@ -142,6 +142,10 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli "boolean": true, "key": "value" }, + "anotherObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", @@ -194,6 +198,10 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli "boolean": true, "key": "value" }, + "anotherObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", diff --git a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt index faf869329be..7a96eac3186 100644 --- a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt +++ b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt @@ -50,6 +50,8 @@ my.debug.cortex-debug.custom.aNumber=[number]10 my.debug.cortex-debug.custom.anotherNumber=[number]10.20 my.debug.cortex-debug.custom.aStringNumber=10 my.debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true} +my.debug.cortex-debug.custom.anotherObject.key=value +my.debug.cortex-debug.custom.anotherObject.boolean=[boolean]true my.debug.svd_file=svd-file my2.name=My Cool Board