This is an automated email from the ASF dual-hosted git repository.

sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git

commit 7793220d7a9526e6cee06949b84feb410ec61163
Author: Szymon Janc <[email protected]>
AuthorDate: Thu Oct 22 00:23:09 2026 +0200

    newtmgr/cli: Add unit tests for res, log and image helpers
    
    Cover the pure helper functions used by the res, log and image
    commands: k=v resource specifier parsing, payload encoding from k=v
    pairs, JSON arguments and raw/JSON/binary payload files, float to int
    conversion for --int, CoAP response formatting, log show argument
    parsing, CBOR log message to JSON conversion, and image flag strings.
    
    Include regression tests for earlier res fixes: k=v pairs must not be
    discarded (80e5c28), and no payload arguments must yield no payload
    rather than an empty CBOR map (d988f6d, 0d6802e).
    
    Assisted-by: Claude:claude-opus-5-5
---
 newtmgr/cli/image_test.go |  51 ++++++
 newtmgr/cli/log_test.go   | 123 +++++++++++++
 newtmgr/cli/res_test.go   | 429 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 603 insertions(+)

diff --git a/newtmgr/cli/image_test.go b/newtmgr/cli/image_test.go
new file mode 100644
index 0000000..453ecdd
--- /dev/null
+++ b/newtmgr/cli/image_test.go
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cli
+
+import (
+       "testing"
+
+       "mynewt.apache.org/newtmgr/nmxact/nmp"
+)
+
+func TestImageFlagsStr(t *testing.T) {
+       tests := []struct {
+               img  nmp.ImageStateEntry
+               want string
+       }{
+               {nmp.ImageStateEntry{}, ""},
+               {nmp.ImageStateEntry{Active: true}, "active"},
+               {nmp.ImageStateEntry{Active: true, Confirmed: true},
+                       "active confirmed"},
+               {nmp.ImageStateEntry{Pending: true, Permanent: true},
+                       "pending permanent"},
+               {nmp.ImageStateEntry{Permanent: true, Pending: true,
+                       Confirmed: true, Active: true, Bootable: true},
+                       "active confirmed pending permanent"},
+               {nmp.ImageStateEntry{Bootable: true}, ""},
+       }
+
+       for _, tt := range tests {
+               if got := imageFlagsStr(tt.img); got != tt.want {
+                       t.Errorf("imageFlagsStr(%+v) = %q, want %q", tt.img, 
got,
+                               tt.want)
+               }
+       }
+}
diff --git a/newtmgr/cli/log_test.go b/newtmgr/cli/log_test.go
new file mode 100644
index 0000000..f28a94d
--- /dev/null
+++ b/newtmgr/cli/log_test.go
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cli
+
+import (
+       "strings"
+       "testing"
+
+       "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+)
+
+func TestLogShowParseArgs(t *testing.T) {
+       tests := []struct {
+               name string
+               args []string
+               want logShowCfg
+       }{
+               {"none", nil, logShowCfg{}},
+               {"name", []string{"reboot_log"}, logShowCfg{Name: 
"reboot_log"}},
+               {"index", []string{"log", "10"}, logShowCfg{Name: "log", Index: 
10}},
+               {"hex index", []string{"log", "0x10"},
+                       logShowCfg{Name: "log", Index: 16}},
+               {"max index", []string{"log", "4294967295"},
+                       logShowCfg{Name: "log", Index: 0xffffffff}},
+               {"last", []string{"log", "last"},
+                       logShowCfg{Name: "log", Timestamp: -1}},
+               {"last ignores timestamp", []string{"log", "last", "junk"},
+                       logShowCfg{Name: "log", Timestamp: -1}},
+               {"index and timestamp", []string{"log", "3", "123456"},
+                       logShowCfg{Name: "log", Index: 3, Timestamp: 123456}},
+               {"negative timestamp", []string{"log", "3", "-7"},
+                       logShowCfg{Name: "log", Index: 3, Timestamp: -7}},
+               {"extra args ignored", []string{"log", "1", "2", "3"},
+                       logShowCfg{Name: "log", Index: 1, Timestamp: 2}},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cfg, err := logShowParseArgs(tt.args)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if *cfg != tt.want {
+                               t.Errorf("got %+v, want %+v", *cfg, tt.want)
+                       }
+               })
+       }
+}
+
+func TestLogShowParseArgsErrors(t *testing.T) {
+       tests := []struct {
+               args []string
+               err  string
+       }{
+               {[]string{"log", "4294967296"}, "index out of range"},
+               {[]string{"log", "-1"}, ""},
+               {[]string{"log", "abc"}, ""},
+               {[]string{"log", "1", "ts"}, ""},
+       }
+
+       for _, tt := range tests {
+               cfg, err := logShowParseArgs(tt.args)
+               if err == nil {
+                       t.Errorf("%v: expected error, got %+v", tt.args, cfg)
+                       continue
+               }
+               if tt.err != "" && err.Error() != tt.err {
+                       t.Errorf("%v: error = %q, want %q", tt.args, 
err.Error(), tt.err)
+               }
+       }
+}
+
+func TestLogCborMsgText(t *testing.T) {
+       b, err := nmxutil.EncodeCbor(map[string]interface{}{
+               "msg": "hello",
+               "n":   7,
+               "neg": -1,
+               "l":   []interface{}{1, "x"},
+       })
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       got, err := logCborMsgText(b)
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       // encoding/json sorts map keys.
+       want := `{"l":[1,"x"],"msg":"hello","n":7,"neg":-1}`
+       if got != want {
+               t.Errorf("got %s, want %s", got, want)
+       }
+}
+
+func TestLogCborMsgTextInvalid(t *testing.T) {
+       // Not a map.
+       if _, err := logCborMsgText([]byte{0x01}); err == nil {
+               t.Errorf("expected error for non-map CBOR")
+       }
+
+       // Truncated map.
+       _, err := logCborMsgText([]byte{0xa1, 0x61})
+       if err == nil || !strings.HasPrefix(err.Error(), "failure decoding 
cbor") {
+               t.Errorf("unexpected error: %v", err)
+       }
+}
diff --git a/newtmgr/cli/res_test.go b/newtmgr/cli/res_test.go
new file mode 100644
index 0000000..239bdb2
--- /dev/null
+++ b/newtmgr/cli/res_test.go
@@ -0,0 +1,429 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cli
+
+import (
+       "os"
+       "path/filepath"
+       "reflect"
+       "strings"
+       "testing"
+
+       "github.com/runtimeco/go-coap"
+
+       "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+)
+
+func TestExtractResKv(t *testing.T) {
+       tests := []struct {
+               name   string
+               params []string
+               want   map[string]interface{}
+       }{
+               {"none", nil, map[string]interface{}{}},
+               {"int", []string{"a=1"}, map[string]interface{}{"a": 1}},
+               {"negative int", []string{"a=-5"}, map[string]interface{}{"a": 
-5}},
+               {"string", []string{"a=hello"},
+                       map[string]interface{}{"a": "hello"}},
+               {"quoted int stays string", []string{`a="12"`},
+                       map[string]interface{}{"a": "12"}},
+               {"quoted bool stays string", []string{`a="true"`},
+                       map[string]interface{}{"a": "true"}},
+               {"empty quoted string", []string{`a=""`},
+                       map[string]interface{}{"a": ""}},
+               {"only leading quote", []string{`a="x`},
+                       map[string]interface{}{"a": `"x`}},
+               {"bool true", []string{"a=true"}, map[string]interface{}{"a": 
true}},
+               {"bool mixed case", []string{"a=FaLsE"},
+                       map[string]interface{}{"a": false}},
+               {"float stays string", []string{"a=1.5"},
+                       map[string]interface{}{"a": "1.5"}},
+               {"empty value", []string{"a="}, map[string]interface{}{"a": 
""}},
+               {"value with equals", []string{"a=b=c"},
+                       map[string]interface{}{"a": "b=c"}},
+               {"empty key", []string{"=1"}, map[string]interface{}{"": 1}},
+               {"multiple", []string{"x=1", "y=two", "z=false"},
+                       map[string]interface{}{"x": 1, "y": "two", "z": false}},
+               {"duplicate key last wins", []string{"x=1", "x=2"},
+                       map[string]interface{}{"x": 2}},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       m, err := extractResKv(tt.params)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if !reflect.DeepEqual(m, tt.want) {
+                               t.Errorf("got %#v, want %#v", m, tt.want)
+                       }
+               })
+       }
+}
+
+func TestExtractResKvErrors(t *testing.T) {
+       for _, p := range []string{"", "abc"} {
+               m, err := extractResKv([]string{"ok=1", p})
+               if err == nil {
+                       t.Errorf("%q: expected error, got %v", p, m)
+                       continue
+               }
+               want := "invalid resource specifier: " + p
+               if err.Error() != want {
+                       t.Errorf("error = %q, want %q", err.Error(), want)
+               }
+       }
+}
+
+// normCbor converts a decoded CBOR value into a form that is easy to compare:
+// maps get string keys and integers become int64.
+func normCbor(v interface{}) interface{} {
+       switch x := v.(type) {
+       case map[interface{}]interface{}:
+               m := map[string]interface{}{}
+               for k, e := range x {
+                       m[k.(string)] = normCbor(e)
+               }
+               return m
+       case []interface{}:
+               for i := range x {
+                       x[i] = normCbor(x[i])
+               }
+               return x
+       case uint64:
+               return int64(x)
+       default:
+               return x
+       }
+}
+
+func decodeCbor(t *testing.T, b []byte) interface{} {
+       t.Helper()
+       v, err := nmxutil.DecodeCbor(b)
+       if err != nil {
+               t.Fatalf("DecodeCbor(%x): %v", b, err)
+       }
+       return normCbor(v)
+}
+
+// setResGlobals sets the res command flag variables for the duration of a
+// test.
+func setResGlobals(t *testing.T, json, toInt bool, jsonFile, rawFile,
+       binFile string) {
+
+       t.Helper()
+
+       oJson, oInt := resJson, resInt
+       oJf, oRf, oBf := resJsonFilename, resRawFilename, resBinFilename
+       t.Cleanup(func() {
+               resJson, resInt = oJson, oInt
+               resJsonFilename, resRawFilename, resBinFilename = oJf, oRf, oBf
+       })
+
+       resJson, resInt = json, toInt
+       resJsonFilename, resRawFilename, resBinFilename = jsonFile, rawFile,
+               binFile
+}
+
+func TestParsePayloadNoArgs(t *testing.T) {
+       // Regression (d988f6d, 0d6802e): no payload arguments must produce no
+       // payload at all, not an empty CBOR map.
+       for _, json := range []bool{false, true} {
+               setResGlobals(t, json, false, "", "", "")
+
+               for _, args := range [][]string{nil, {}} {
+                       b, err := parsePayload(args)
+                       if err != nil {
+                               t.Fatalf("json=%v: unexpected error: %v", json, 
err)
+                       }
+                       if b != nil {
+                               t.Errorf("json=%v: payload = %x, want nil", 
json, b)
+                       }
+
+                       b, err = calcCborPayload(args)
+                       if err != nil {
+                               t.Fatalf("json=%v: unexpected error: %v", json, 
err)
+                       }
+                       if b != nil {
+                               t.Errorf("json=%v: payload = %x, want nil", 
json, b)
+                       }
+               }
+       }
+}
+
+func TestParsePayloadKv(t *testing.T) {
+       setResGlobals(t, false, false, "", "", "")
+
+       // Regression (80e5c28): k=v pairs must not be discarded.
+       b, err := parsePayload([]string{"a=1", "b=str", "c=true", `d="7"`})
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       want := map[string]interface{}{
+               "a": int64(1), "b": "str", "c": true, "d": "7",
+       }
+       if got := decodeCbor(t, b); !reflect.DeepEqual(got, want) {
+               t.Errorf("payload = %#v, want %#v", got, want)
+       }
+
+       if _, err := parsePayload([]string{"novalue"}); err == nil {
+               t.Errorf("expected error for invalid specifier")
+       }
+}
+
+func TestParsePayloadJsonArg(t *testing.T) {
+       tests := []struct {
+               name  string
+               toInt bool
+               arg   string
+               want  interface{}
+       }{
+               {"object float", false, `{"a": 2, "b": [1.5, "x"]}`,
+                       map[string]interface{}{
+                               "a": float64(2),
+                               "b": []interface{}{1.5, "x"},
+                       }},
+               {"object int", true, `{"a": 2, "b": [1.5, 3.0, {"c": -4}]}`,
+                       map[string]interface{}{
+                               "a": int64(2),
+                               "b": []interface{}{1.5, int64(3),
+                                       map[string]interface{}{"c": int64(-4)}},
+                       }},
+               {"scalar", true, `5`, int64(5)},
+               {"string", false, `"hi"`, "hi"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       setResGlobals(t, true, tt.toInt, "", "", "")
+
+                       b, err := parsePayload([]string{tt.arg, "ignored"})
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if got := decodeCbor(t, b); !reflect.DeepEqual(got, 
tt.want) {
+                               t.Errorf("payload = %#v, want %#v", got, 
tt.want)
+                       }
+               })
+       }
+}
+
+func TestParsePayloadJsonNull(t *testing.T) {
+       setResGlobals(t, true, false, "", "", "")
+
+       // A JSON null is treated as "no payload".
+       b, err := parsePayload([]string{"null"})
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       if b != nil {
+               t.Errorf("payload = %x, want nil", b)
+       }
+}
+
+func TestParsePayloadJsonInvalid(t *testing.T) {
+       setResGlobals(t, true, false, "", "", "")
+
+       if _, err := parsePayload([]string{"{bad"}); err == nil {
+               t.Errorf("expected error for invalid JSON")
+       }
+       if _, err := parsePayloadJson(""); err == nil {
+               t.Errorf("expected error for empty JSON")
+       }
+}
+
+func TestRemoveFloats(t *testing.T) {
+       tests := []struct {
+               in   interface{}
+               want interface{}
+       }{
+               {float64(2), int64(2)},
+               {float64(-3), int64(-3)},
+               {float64(0), int64(0)},
+               {1.5, 1.5},
+               {float32(4), int64(4)},
+               {float32(0.5), float64(0.5)},
+               {"s", "s"},
+               {true, true},
+               {nil, nil},
+               {[]interface{}{1.0, 2.5, "x"}, []interface{}{int64(1), 2.5, 
"x"}},
+               {map[string]interface{}{"a": 1.0, "b": []interface{}{7.0}},
+                       map[string]interface{}{"a": int64(1),
+                               "b": []interface{}{int64(7)}}},
+       }
+
+       for _, tt := range tests {
+               got := removeFloats(tt.in)
+               if !reflect.DeepEqual(got, tt.want) {
+                       t.Errorf("removeFloats(%#v) = %#v, want %#v", tt.in, 
got, tt.want)
+               }
+       }
+}
+
+func writeTemp(t *testing.T, name string, data []byte) string {
+       t.Helper()
+       p := filepath.Join(t.TempDir(), name)
+       if err := os.WriteFile(p, data, 0644); err != nil {
+               t.Fatal(err)
+       }
+       return p
+}
+
+func TestCalcCborPayloadRawFile(t *testing.T) {
+       raw := []byte{0xa1, 0x61, 0x61, 0x01, 0xff, 0x00}
+       setResGlobals(t, false, false, "", writeTemp(t, "raw", raw), "")
+
+       // The raw file is sent verbatim and arguments are ignored.
+       b, err := calcCborPayload([]string{"a=2"})
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       if !reflect.DeepEqual(b, raw) {
+               t.Errorf("payload = %x, want %x", b, raw)
+       }
+}
+
+func TestCalcCborPayloadJsonFile(t *testing.T) {
+       p := writeTemp(t, "j.json", []byte(`{"a": 3.0, "b": "x"}`))
+       setResGlobals(t, false, true, p, "", "")
+
+       b, err := calcCborPayload(nil)
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       want := map[string]interface{}{"a": int64(3), "b": "x"}
+       if got := decodeCbor(t, b); !reflect.DeepEqual(got, want) {
+               t.Errorf("payload = %#v, want %#v", got, want)
+       }
+
+       bad := writeTemp(t, "bad.json", []byte(`{`))
+       setResGlobals(t, false, false, bad, "", "")
+       if _, err := calcCborPayload(nil); err == nil {
+               t.Errorf("expected error for invalid JSON file")
+       }
+}
+
+func TestCalcCborPayloadBinFile(t *testing.T) {
+       data := []byte{0, 1, 2, 0xfe}
+       setResGlobals(t, false, false, "", "", writeTemp(t, "bin", data))
+
+       b, err := calcCborPayload(nil)
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       // Encoded as a CBOR byte string.
+       want := append([]byte{0x44}, data...)
+       if !reflect.DeepEqual(b, want) {
+               t.Errorf("payload = %x, want %x", b, want)
+       }
+}
+
+func TestCalcCborPayloadMissingFile(t *testing.T) {
+       missing := filepath.Join(t.TempDir(), "missing")
+
+       cases := []struct {
+               name              string
+               jsonF, rawF, binF string
+       }{
+               {"json", missing, "", ""},
+               {"raw", "", missing, ""},
+               {"bin", "", "", missing},
+       }
+       for _, c := range cases {
+               setResGlobals(t, false, false, c.jsonF, c.rawF, c.binF)
+               if _, err := calcCborPayload(nil); err == nil {
+                       t.Errorf("%s: expected error for missing file", c.name)
+               }
+       }
+}
+
+func TestCborValStr(t *testing.T) {
+       tests := []struct {
+               in   interface{}
+               want string
+       }{
+               {"abc", "abc"},
+               {uint64(5), "0x5"},
+               {int64(-5), "-5"},
+               {true, "true"},
+               {[]byte{0x01, 0x02},
+                       "00000000  01 02                                        
     |..|"},
+       }
+
+       for _, tt := range tests {
+               if got := cborValStr(tt.in); got != tt.want {
+                       t.Errorf("cborValStr(%#v) = %q, want %q", tt.in, got, 
tt.want)
+               }
+       }
+}
+
+func TestPrintCode(t *testing.T) {
+       tests := []struct {
+               code coap.COAPCode
+               want string
+       }{
+               {coap.Content, "CoAP Response Code: 2.05 Content\n"},
+               {coap.NotFound, "CoAP Response Code: 4.04 NotFound\n"},
+               {coap.Created, "CoAP Response Code: 2.01 Created\n"},
+       }
+
+       for _, tt := range tests {
+               if got := printCode(tt.code); got != tt.want {
+                       t.Errorf("printCode(%d) = %q, want %q", tt.code, got, 
tt.want)
+               }
+       }
+}
+
+func TestCleanUpMapValue(t *testing.T) {
+       in := map[interface{}]interface{}{
+               "s":       "str",
+               uint64(1): []interface{}{int64(-1), true, nil},
+               "m":       map[interface{}]interface{}{"x": 1.5},
+       }
+       want := map[string]interface{}{
+               "s": "str",
+               "1": []interface{}{"-1", "true", "<nil>"},
+               "m": map[string]interface{}{"x": "1.5"},
+       }
+
+       if got := cleanUpMapValue(in); !reflect.DeepEqual(got, want) {
+               t.Errorf("got %#v, want %#v", got, want)
+       }
+}
+
+func TestResResponseStr(t *testing.T) {
+       if got := resResponseStr("/a/b", nil); got != "/a/b\n    <empty>" {
+               t.Errorf("empty payload: got %q", got)
+       }
+
+       b, err := nmxutil.EncodeCbor(map[string]interface{}{"k": 1})
+       if err != nil {
+               t.Fatal(err)
+       }
+       want := "/a/b\n{\n    \"k\": \"1\"\n}"
+       if got := resResponseStr("/a/b", b); got != want {
+               t.Errorf("got %q, want %q", got, want)
+       }
+
+       got := resResponseStr("/a/b", []byte{0xff, 0xff})
+       if !strings.HasPrefix(got, "/a/b\n    invalid incoming cbor:") {
+               t.Errorf("invalid cbor: got %q", got)
+       }
+}

Reply via email to