Skip to content

Commit 64f1853

Browse files
authored
debug: Allow type-specification of JSON output for cortex-debug (#2393)
* debug: Allow type-specification of JSON output for cortex-debug * Improved JSON properties generation
1 parent 0c0573f commit 64f1853

File tree

5 files changed

+242
-23
lines changed

5 files changed

+242
-23
lines changed

commands/debug/debug_info.go

+80-23
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ package debug
1818
import (
1919
"context"
2020
"encoding/json"
21-
"regexp"
21+
"slices"
22+
"strconv"
2223
"strings"
2324

2425
"github.com/arduino/arduino-cli/arduino"
@@ -210,33 +211,89 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl
210211
// my.indexed.array.2=third
211212
//
212213
// into the corresponding JSON arrays.
214+
// If a value should be converted into a JSON type different from string, the value
215+
// may be prefiex with "[boolean]", "[number]", or "[object]":
216+
//
217+
// my.stringValue=a string
218+
// my.booleanValue=[boolean]true
219+
// my.numericValue=[number]20
213220
func convertToJsonMap(in *properties.Map) string {
214-
// XXX: Maybe this method could be a good candidate for propertis.Map?
215-
216-
// Find the values that should be kept as is, and the indexed arrays
217-
// that should be later converted into arrays.
218-
arraysKeys := map[string]bool{}
219-
stringKeys := []string{}
220-
trailingNumberMatcher := regexp.MustCompile(`^(.*)\.[0-9]+$`)
221-
for _, k := range in.Keys() {
222-
match := trailingNumberMatcher.FindAllStringSubmatch(k, -1)
223-
if len(match) > 0 && len(match[0]) > 1 {
224-
arraysKeys[match[0][1]] = true
225-
} else {
226-
stringKeys = append(stringKeys, k)
221+
data, _ := json.MarshalIndent(convertToRawInterface(in), "", " ")
222+
return string(data)
223+
}
224+
225+
func allNumerics(in []string) bool {
226+
for _, i := range in {
227+
for _, c := range i {
228+
if c < '0' || c > '9' {
229+
return false
230+
}
227231
}
228232
}
233+
return true
234+
}
229235

230-
// Compose a map that can be later marshaled into JSON keeping
231-
// the arrays where they are expected to be.
232-
res := map[string]any{}
233-
for _, k := range stringKeys {
234-
res[k] = in.Get(k)
236+
func convertToRawInterface(in *properties.Map) any {
237+
subtrees := in.FirstLevelOf()
238+
keys := in.FirstLevelKeys()
239+
240+
if allNumerics(keys) {
241+
// Compose an array
242+
res := []any{}
243+
slices.SortFunc(keys, func(x, y string) int {
244+
nx, _ := strconv.Atoi(x)
245+
ny, _ := strconv.Atoi(y)
246+
return nx - ny
247+
})
248+
for _, k := range keys {
249+
switch {
250+
case subtrees[k] != nil:
251+
res = append(res, convertToRawInterface(subtrees[k]))
252+
default:
253+
res = append(res, convertToRawValue(in.Get(k)))
254+
}
255+
}
256+
return res
235257
}
236-
for k := range arraysKeys {
237-
res[k] = in.ExtractSubIndexLists(k)
258+
259+
// Compose an object
260+
res := map[string]any{}
261+
for _, k := range keys {
262+
switch {
263+
case subtrees[k] != nil:
264+
res[k] = convertToRawInterface(subtrees[k])
265+
default:
266+
res[k] = convertToRawValue(in.Get(k))
267+
}
238268
}
269+
return res
270+
}
239271

240-
data, _ := json.MarshalIndent(res, "", " ")
241-
return string(data)
272+
func convertToRawValue(v string) any {
273+
switch {
274+
case strings.HasPrefix(v, "[boolean]"):
275+
v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]"))
276+
if strings.EqualFold(v, "true") {
277+
return true
278+
} else if strings.EqualFold(v, "false") {
279+
return false
280+
}
281+
case strings.HasPrefix(v, "[number]"):
282+
v = strings.TrimPrefix(v, "[number]")
283+
if i, err := strconv.Atoi(v); err == nil {
284+
return i
285+
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
286+
return f
287+
}
288+
case strings.HasPrefix(v, "[object]"):
289+
v = strings.TrimPrefix(v, "[object]")
290+
var o interface{}
291+
if err := json.Unmarshal([]byte(v), &o); err == nil {
292+
return o
293+
}
294+
case strings.HasPrefix(v, "[string]"):
295+
v = strings.TrimPrefix(v, "[string]")
296+
}
297+
// default or conversion error, return string as is
298+
return v
242299
}

commands/debug/debug_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
2525
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2626
"github.com/arduino/go-paths-helper"
27+
"github.com/arduino/go-properties-orderedmap"
2728
"github.com/stretchr/testify/assert"
2829
"github.com/stretchr/testify/require"
2930
)
@@ -106,3 +107,99 @@ func TestGetCommandLine(t *testing.T) {
106107
commandToTest2 := strings.Join(command2, " ")
107108
assert.Equal(t, filepath.FromSlash(goldCommand2), filepath.FromSlash(commandToTest2))
108109
}
110+
111+
func TestConvertToJSONMap(t *testing.T) {
112+
testIn := properties.NewFromHashmap(map[string]string{
113+
"k": "v",
114+
"string": "[string]aaa",
115+
"bool": "[boolean]true",
116+
"number": "[number]10",
117+
"number2": "[number]10.2",
118+
"object": `[object]{ "key":"value", "bool":true }`,
119+
"array.0": "first",
120+
"array.1": "second",
121+
"array.2": "[boolean]true",
122+
"array.3": "[number]10",
123+
"array.4": `[object]{ "key":"value", "bool":true }`,
124+
"array.5.k": "v",
125+
"array.5.bool": "[boolean]true",
126+
"array.5.number": "[number]10",
127+
"array.5.number2": "[number]10.2",
128+
"array.5.object": `[object]{ "key":"value", "bool":true }`,
129+
"array.6.sub.k": "v",
130+
"array.6.sub.bool": "[boolean]true",
131+
"array.6.sub.number": "[number]10",
132+
"array.6.sub.number2": "[number]10.2",
133+
"array.6.sub.object": `[object]{ "key":"value", "bool":true }`,
134+
"array.7.0": "v",
135+
"array.7.1": "[boolean]true",
136+
"array.7.2": "[number]10",
137+
"array.7.3": "[number]10.2",
138+
"array.7.4": `[object]{ "key":"value", "bool":true }`,
139+
"array.8.array.0": "v",
140+
"array.8.array.1": "[boolean]true",
141+
"array.8.array.2": "[number]10",
142+
"array.8.array.3": "[number]10.2",
143+
"array.8.array.4": `[object]{ "key":"value", "bool":true }`,
144+
"sub.k": "v",
145+
"sub.bool": "[boolean]true",
146+
"sub.number": "[number]10",
147+
"sub.number2": "[number]10.2",
148+
"sub.object": `[object]{ "key":"value", "bool":true }`,
149+
})
150+
jsonString := convertToJsonMap(testIn)
151+
require.JSONEq(t, `{
152+
"k": "v",
153+
"string": "aaa",
154+
"bool": true,
155+
"number": 10,
156+
"number2": 10.2,
157+
"object": { "key":"value", "bool":true },
158+
"array": [
159+
"first",
160+
"second",
161+
true,
162+
10,
163+
{ "key":"value", "bool":true },
164+
{
165+
"k": "v",
166+
"bool": true,
167+
"number": 10,
168+
"number2": 10.2,
169+
"object": { "key":"value", "bool":true }
170+
},
171+
{
172+
"sub": {
173+
"k": "v",
174+
"bool": true,
175+
"number": 10,
176+
"number2": 10.2,
177+
"object": { "key":"value", "bool":true }
178+
}
179+
},
180+
[
181+
"v",
182+
true,
183+
10,
184+
10.2,
185+
{ "key":"value", "bool":true }
186+
],
187+
{
188+
"array": [
189+
"v",
190+
true,
191+
10,
192+
10.2,
193+
{ "key":"value", "bool":true }
194+
]
195+
}
196+
],
197+
"sub": {
198+
"k": "v",
199+
"bool": true,
200+
"number": 10,
201+
"number2": 10.2,
202+
"object": { "key":"value", "bool":true }
203+
}
204+
}`, jsonString)
205+
}

docs/platform-specification.md

+31
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,37 @@ will result in the following JSON to be merged in the Arduino IDE generated `lau
14031403
}
14041404
```
14051405

1406+
All the values are converted by default to a string in the resulting JSON. If another type is needed the value can be
1407+
prefixed with the tags `[boolean]`, `[number]`, `[string]` or `[object]` to force a specific type in the JSON. Moreover
1408+
the hierarchy of the properties may be used to build JSON objects. For example:
1409+
1410+
```
1411+
debug.cortex-debug.custom.aBoolean=[boolean]true
1412+
debug.cortex-debug.custom.aNumber=[number]10
1413+
debug.cortex-debug.custom.anotherNumber=[number]10.20
1414+
debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true}
1415+
debug.cortex-debug.custom.anotherObject.key=value
1416+
debug.cortex-debug.custom.anotherObject.boolean=[boolean]true
1417+
```
1418+
1419+
will result in the following JSON:
1420+
1421+
```json
1422+
{
1423+
"aBoolean": true,
1424+
"aNumber": 10,
1425+
"anotherNumber": 10.2,
1426+
"anObject": {
1427+
"boolean": true,
1428+
"key": "value"
1429+
},
1430+
"anotherObject": {
1431+
"boolean": true,
1432+
"key": "value"
1433+
}
1434+
}
1435+
```
1436+
14061437
### Optimization level for debugging
14071438

14081439
The compiler optimization level that is appropriate for normal usage will often not provide a good experience while

internal/integrationtest/debug/debug_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
133133
},
134134
"svd_file": "svd-file",
135135
"cortex-debug_custom_configuration": {
136+
"aBoolean": true,
137+
"aStringBoolean": "true",
138+
"aStringNumber": "10",
139+
"aNumber": 10,
140+
"anotherNumber": 10.2,
141+
"anObject": {
142+
"boolean": true,
143+
"key": "value"
144+
},
145+
"anotherObject": {
146+
"boolean": true,
147+
"key": "value"
148+
},
136149
"anotherStringParamer": "hellooo",
137150
"overrideRestartCommands": [
138151
"monitor reset halt",
@@ -176,6 +189,19 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
176189
},
177190
"svd_file": "svd-file",
178191
"cortex-debug_custom_configuration": {
192+
"aBoolean": true,
193+
"aStringBoolean": "true",
194+
"aStringNumber": "10",
195+
"aNumber": 10,
196+
"anotherNumber": 10.2,
197+
"anObject": {
198+
"boolean": true,
199+
"key": "value"
200+
},
201+
"anotherObject": {
202+
"boolean": true,
203+
"key": "value"
204+
},
179205
"anotherStringParamer": "hellooo",
180206
"overrideRestartCommands": [
181207
"monitor reset halt",

internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ my.debug.cortex-debug.custom.overrideRestartCommands.1=monitor gdb_sync
4444
my.debug.cortex-debug.custom.overrideRestartCommands.2=thb setup
4545
my.debug.cortex-debug.custom.overrideRestartCommands.3=c
4646
my.debug.cortex-debug.custom.anotherStringParamer=hellooo
47+
my.debug.cortex-debug.custom.aBoolean=[boolean]true
48+
my.debug.cortex-debug.custom.aStringBoolean=true
49+
my.debug.cortex-debug.custom.aNumber=[number]10
50+
my.debug.cortex-debug.custom.anotherNumber=[number]10.20
51+
my.debug.cortex-debug.custom.aStringNumber=10
52+
my.debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true}
53+
my.debug.cortex-debug.custom.anotherObject.key=value
54+
my.debug.cortex-debug.custom.anotherObject.boolean=[boolean]true
4755
my.debug.svd_file=svd-file
4856

4957
my2.name=My Cool Board

0 commit comments

Comments
 (0)