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) + } +}
