// Copyright (c) 2014 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package btcjson_test import ( "reflect" "testing" "github.com/btcsuite/btcd/btcjson" ) // TestHelpReflectInternals ensures the various help functions which deal with // reflect types work as expected for various Go types. func TestHelpReflectInternals(t *testing.T) { t.Parallel() tests := []struct { name string reflectType reflect.Type indentLevel int key string examples []string isComplex bool help string isInvalid bool }{ { name: "int", reflectType: reflect.TypeOf(int(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "*int", reflectType: reflect.TypeOf((*int)(nil)), key: "json-type-value", examples: []string{"n"}, help: "n (json-type-value) fdk", isInvalid: true, }, { name: "int8", reflectType: reflect.TypeOf(int8(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "int16", reflectType: reflect.TypeOf(int16(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "int32", reflectType: reflect.TypeOf(int32(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "int64", reflectType: reflect.TypeOf(int64(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "uint", reflectType: reflect.TypeOf(uint(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "uint8", reflectType: reflect.TypeOf(uint8(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "uint16", reflectType: reflect.TypeOf(uint16(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "uint32", reflectType: reflect.TypeOf(uint32(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "uint64", reflectType: reflect.TypeOf(uint64(0)), key: "json-type-numeric", examples: []string{"n"}, help: "n (json-type-numeric) fdk", }, { name: "float32", reflectType: reflect.TypeOf(float32(0)), key: "json-type-numeric", examples: []string{"n.nnn"}, help: "n.nnn (json-type-numeric) fdk", }, { name: "float64", reflectType: reflect.TypeOf(float64(0)), key: "json-type-numeric", examples: []string{"n.nnn"}, help: "n.nnn (json-type-numeric) fdk", }, { name: "string", reflectType: reflect.TypeOf(""), key: "json-type-string", examples: []string{`"json-example-string"`}, help: "\"json-example-string\" (json-type-string) fdk", }, { name: "bool", reflectType: reflect.TypeOf(true), key: "json-type-bool", examples: []string{"json-example-bool"}, help: "json-example-bool (json-type-bool) fdk", }, { name: "array of int", reflectType: reflect.TypeOf([1]int{0}), key: "json-type-arrayjson-type-numeric", examples: []string{"[n,...]"}, help: "[n,...] (json-type-arrayjson-type-numeric) fdk", }, { name: "slice of int", reflectType: reflect.TypeOf([]int{0}), key: "json-type-arrayjson-type-numeric", examples: []string{"[n,...]"}, help: "[n,...] (json-type-arrayjson-type-numeric) fdk", }, { name: "struct", reflectType: reflect.TypeOf(struct{}{}), key: "json-type-object", examples: []string{"{", "}\t\t"}, isComplex: true, help: "{\n} ", }, { name: "struct indent level 1", reflectType: reflect.TypeOf(struct{ field int }{}), indentLevel: 1, key: "json-type-object", examples: []string{ " \"field\": n,\t(json-type-numeric)\t-field", " },\t\t", }, help: "{\n" + " \"field\": n, (json-type-numeric) -field\n" + "} ", isComplex: true, }, { name: "array of struct indent level 0", reflectType: func() reflect.Type { type s struct { field int } return reflect.TypeOf([]s{}) }(), key: "json-type-arrayjson-type-object", examples: []string{ "[{", " \"field\": n,\t(json-type-numeric)\ts-field", "},...]", }, help: "[{\n" + " \"field\": n, (json-type-numeric) s-field\n" + "},...]", isComplex: true, }, { name: "array of struct indent level 1", reflectType: func() reflect.Type { type s struct { field int } return reflect.TypeOf([]s{}) }(), indentLevel: 1, key: "json-type-arrayjson-type-object", examples: []string{ " \"field\": n,\t(json-type-numeric)\ts-field", " },...],\t\t", }, help: "[{\n" + " \"field\": n, (json-type-numeric) s-field\n" + "},...]", isComplex: true, }, { name: "map", reflectType: reflect.TypeOf(map[string]string{}), key: "json-type-object", examples: []string{"{", " \"fdk--key\": fdk--value, (json-type-object) fdk--desc", " ...", "}", }, help: "{\n" + " \"fdk--key\": fdk--value, (json-type-object) fdk--desc\n" + " ...\n" + "}", isComplex: true, }, { name: "complex", reflectType: reflect.TypeOf(complex64(0)), key: "json-type-value", examples: []string{"json-example-unknown"}, help: "json-example-unknown (json-type-value) fdk", isInvalid: true, }, } xT := func(key string) string { return key } t.Logf("Running %d tests", len(tests)) for i, test := range tests { // Ensure the description key is the expected value. key := btcjson.TstReflectTypeToJSONType(xT, test.reflectType) if key != test.key { t.Errorf("Test #%d (%s) unexpected key - got: %v, "+ "want: %v", i, test.name, key, test.key) continue } // Ensure the generated example is as expected. examples, isComplex := btcjson.TstReflectTypeToJSONExample(xT, test.reflectType, test.indentLevel, "fdk") if isComplex != test.isComplex { t.Errorf("Test #%d (%s) unexpected isComplex - got: %v, "+ "want: %v", i, test.name, isComplex, test.isComplex) continue } if len(examples) != len(test.examples) { t.Errorf("Test #%d (%s) unexpected result length - "+ "got: %v, want: %v", i, test.name, len(examples), len(test.examples)) continue } for j, example := range examples { if example != test.examples[j] { t.Errorf("Test #%d (%s) example #%d unexpected "+ "example - got: %v, want: %v", i, test.name, j, example, test.examples[j]) continue } } // Ensure the generated result type help is as expected. helpText := btcjson.TstResultTypeHelp(xT, test.reflectType, "fdk") if helpText != test.help { t.Errorf("Test #%d (%s) unexpected result help - "+ "got: %v, want: %v", i, test.name, helpText, test.help) continue } isValid := btcjson.TstIsValidResultType(test.reflectType.Kind()) if isValid != !test.isInvalid { t.Errorf("Test #%d (%s) unexpected result type validity "+ "- got: %v", i, test.name, isValid) continue } } } // TestResultStructHelp ensures the expected help text format is returned for // various Go struct types. func TestResultStructHelp(t *testing.T) { t.Parallel() tests := []struct { name string reflectType reflect.Type expected []string }{ { name: "empty struct", reflectType: func() reflect.Type { type s struct{} return reflect.TypeOf(s{}) }(), expected: nil, }, { name: "struct with primitive field", reflectType: func() reflect.Type { type s struct { field int } return reflect.TypeOf(s{}) }(), expected: []string{ "\"field\": n,\t(json-type-numeric)\ts-field", }, }, { name: "struct with primitive field and json tag", reflectType: func() reflect.Type { type s struct { Field int `json:"f"` } return reflect.TypeOf(s{}) }(), expected: []string{ "\"f\": n,\t(json-type-numeric)\ts-f", }, }, { name: "struct with array of primitive field", reflectType: func() reflect.Type { type s struct { field []int } return reflect.TypeOf(s{}) }(), expected: []string{ "\"field\": [n,...],\t(json-type-arrayjson-type-numeric)\ts-field", }, }, { name: "struct with sub-struct field", reflectType: func() reflect.Type { type s2 struct { subField int } type s struct { field s2 } return reflect.TypeOf(s{}) }(), expected: []string{ "\"field\": {\t(json-type-object)\ts-field", "{", " \"subfield\": n,\t(json-type-numeric)\ts2-subfield", "}\t\t", }, }, { name: "struct with sub-struct field pointer", reflectType: func() reflect.Type { type s2 struct { subField int } type s struct { field *s2 } return reflect.TypeOf(s{}) }(), expected: []string{ "\"field\": {\t(json-type-object)\ts-field", "{", " \"subfield\": n,\t(json-type-numeric)\ts2-subfield", "}\t\t", }, }, { name: "struct with array of structs field", reflectType: func() reflect.Type { type s2 struct { subField int } type s struct { field []s2 } return reflect.TypeOf(s{}) }(), expected: []string{ "\"field\": [{\t(json-type-arrayjson-type-object)\ts-field", "[{", " \"subfield\": n,\t(json-type-numeric)\ts2-subfield", "},...]", }, }, } xT := func(key string) string { return key } t.Logf("Running %d tests", len(tests)) for i, test := range tests { results := btcjson.TstResultStructHelp(xT, test.reflectType, 0) if len(results) != len(test.expected) { t.Errorf("Test #%d (%s) unexpected result length - "+ "got: %v, want: %v", i, test.name, len(results), len(test.expected)) continue } for j, result := range results { if result != test.expected[j] { t.Errorf("Test #%d (%s) result #%d unexpected "+ "result - got: %v, want: %v", i, test.name, j, result, test.expected[j]) continue } } } } // TestHelpArgInternals ensures the various help functions which deal with // arguments work as expected for various argument types. func TestHelpArgInternals(t *testing.T) { t.Parallel() tests := []struct { name string method string reflectType reflect.Type defaults map[int]reflect.Value help string }{ { name: "command with no args", method: "test", reflectType: func() reflect.Type { type s struct{} return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "", }, { name: "command with one required arg", method: "test", reflectType: func() reflect.Type { type s struct { Field int } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. field (json-type-numeric, help-required) test-field\n", }, { name: "command with one optional arg, no default", method: "test", reflectType: func() reflect.Type { type s struct { Optional *int } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. optional (json-type-numeric, help-optional) test-optional\n", }, { name: "command with one optional arg with default", method: "test", reflectType: func() reflect.Type { type s struct { Optional *string } return reflect.TypeOf((*s)(nil)) }(), defaults: func() map[int]reflect.Value { defVal := "test" return map[int]reflect.Value{ 0: reflect.ValueOf(&defVal), } }(), help: "1. optional (json-type-string, help-optional, help-default=\"test\") test-optional\n", }, { name: "command with struct field", method: "test", reflectType: func() reflect.Type { type s2 struct { F int8 } type s struct { Field s2 } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. field (json-type-object, help-required) test-field\n" + "{\n" + " \"f\": n, (json-type-numeric) s2-f\n" + "} \n", }, { name: "command with map field", method: "test", reflectType: func() reflect.Type { type s struct { Field map[string]float64 } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. field (json-type-object, help-required) test-field\n" + "{\n" + " \"test-field--key\": test-field--value, (json-type-object) test-field--desc\n" + " ...\n" + "}\n", }, { name: "command with slice of primitives field", method: "test", reflectType: func() reflect.Type { type s struct { Field []int64 } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. field (json-type-arrayjson-type-numeric, help-required) test-field\n", }, { name: "command with slice of structs field", method: "test", reflectType: func() reflect.Type { type s2 struct { F int64 } type s struct { Field []s2 } return reflect.TypeOf((*s)(nil)) }(), defaults: nil, help: "1. field (json-type-arrayjson-type-object, help-required) test-field\n" + "[{\n" + " \"f\": n, (json-type-numeric) s2-f\n" + "},...]\n", }, } xT := func(key string) string { return key } t.Logf("Running %d tests", len(tests)) for i, test := range tests { help := btcjson.TstArgHelp(xT, test.reflectType, test.defaults, test.method) if help != test.help { t.Errorf("Test #%d (%s) unexpected help - got:\n%v\n"+ "want:\n%v", i, test.name, help, test.help) continue } } } // TestMethodHelp ensures the method help function works as expected for various // command structs. func TestMethodHelp(t *testing.T) { t.Parallel() tests := []struct { name string method string reflectType reflect.Type defaults map[int]reflect.Value resultTypes []interface{} help string }{ { name: "command with no args or results", method: "test", reflectType: func() reflect.Type { type s struct{} return reflect.TypeOf((*s)(nil)) }(), help: "test\n\ntest--synopsis\n\n" + "help-arguments:\nhelp-arguments-none\n\n" + "help-result:\nhelp-result-nothing\n", }, { name: "command with no args and one primitive result", method: "test", reflectType: func() reflect.Type { type s struct{} return reflect.TypeOf((*s)(nil)) }(), resultTypes: []interface{}{(*int64)(nil)}, help: "test\n\ntest--synopsis\n\n" + "help-arguments:\nhelp-arguments-none\n\n" + "help-result:\nn (json-type-numeric) test--result0\n", }, { name: "command with no args and two results", method: "test", reflectType: func() reflect.Type { type s struct{} return reflect.TypeOf((*s)(nil)) }(), resultTypes: []interface{}{(*int64)(nil), nil}, help: "test\n\ntest--synopsis\n\n" + "help-arguments:\nhelp-arguments-none\n\n" + "help-result (test--condition0):\nn (json-type-numeric) test--result0\n\n" + "help-result (test--condition1):\nhelp-result-nothing\n", }, { name: "command with primitive arg and no results", method: "test", reflectType: func() reflect.Type { type s struct { Field bool } return reflect.TypeOf((*s)(nil)) }(), help: "test field\n\ntest--synopsis\n\n" + "help-arguments:\n1. field (json-type-bool, help-required) test-field\n\n" + "help-result:\nhelp-result-nothing\n", }, { name: "command with primitive optional and no results", method: "test", reflectType: func() reflect.Type { type s struct { Field *bool } return reflect.TypeOf((*s)(nil)) }(), help: "test (field)\n\ntest--synopsis\n\n" + "help-arguments:\n1. field (json-type-bool, help-optional) test-field\n\n" + "help-result:\nhelp-result-nothing\n", }, } xT := func(key string) string { return key } t.Logf("Running %d tests", len(tests)) for i, test := range tests { help := btcjson.TestMethodHelp(xT, test.reflectType, test.defaults, test.method, test.resultTypes) if help != test.help { t.Errorf("Test #%d (%s) unexpected help - got:\n%v\n"+ "want:\n%v", i, test.name, help, test.help) continue } } } // TestGenerateHelpErrors ensures the GenerateHelp function returns the expected // errors. func TestGenerateHelpErrors(t *testing.T) { t.Parallel() tests := []struct { name string method string resultTypes []interface{} err btcjson.Error }{ { name: "unregistered command", method: "boguscommand", err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, }, { name: "non-pointer result type", method: "help", resultTypes: []interface{}{0}, err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, }, { name: "invalid result type", method: "help", resultTypes: []interface{}{(*complex64)(nil)}, err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, }, { name: "missing description", method: "help", resultTypes: []interface{}{(*string)(nil), nil}, err: btcjson.Error{ErrorCode: btcjson.ErrMissingDescription}, }, } t.Logf("Running %d tests", len(tests)) for i, test := range tests { _, err := btcjson.GenerateHelp(test.method, nil, test.resultTypes...) if reflect.TypeOf(err) != reflect.TypeOf(test.err) { t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+ "want %T", i, test.name, err, test.err) continue } gotErrorCode := err.(btcjson.Error).ErrorCode if gotErrorCode != test.err.ErrorCode { t.Errorf("Test #%d (%s) mismatched error code - got "+ "%v (%v), want %v", i, test.name, gotErrorCode, err, test.err.ErrorCode) continue } } } // TestGenerateHelp performs a very basic test to ensure GenerateHelp is working // as expected. The internal are testd much more thoroughly in other tests, so // there is no need to add more tests here. func TestGenerateHelp(t *testing.T) { t.Parallel() descs := map[string]string{ "help--synopsis": "test", "help-command": "test", } help, err := btcjson.GenerateHelp("help", descs) if err != nil { t.Fatalf("GenerateHelp: unexpected error: %v", err) } wantHelp := "help (\"command\")\n\n" + "test\n\nArguments:\n1. command (string, optional) test\n\n" + "Result:\nNothing\n" if help != wantHelp { t.Fatalf("GenerateHelp: unexpected help - got\n%v\nwant\n%v", help, wantHelp) } }