package btcjson_test import ( "reflect" "testing" "github.com/p9c/p9/pkg/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(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" + "help-arguments:\nhelp-arguments-none\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" + "help-arguments:\nhelp-arguments-none\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" + "help-arguments:\nhelp-arguments-none\n" + "help-result (test--condition0):\nn (json-type-numeric) test--result0\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" + "help-arguments:\n1. field (json-type-bool, help-required) test-field\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" + "help-arguments:\n1. field (json-type-bool, help-optional) test-field\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{} e btcjson.GeneralError }{ { name: "unregistered command", method: "boguscommand", e: btcjson.GeneralError{ErrorCode: btcjson.ErrUnregisteredMethod}, }, { name: "non-pointer result type", method: "help", resultTypes: []interface{}{0}, e: btcjson.GeneralError{ErrorCode: btcjson.ErrInvalidType}, }, { name: "invalid result type", method: "help", resultTypes: []interface{}{(*complex64)(nil)}, e: btcjson.GeneralError{ErrorCode: btcjson.ErrInvalidType}, }, { name: "missing description", method: "help", resultTypes: []interface{}{(*string)(nil), nil}, e: btcjson.GeneralError{ErrorCode: btcjson.ErrMissingDescription}, }, } t.Logf("Running %d tests", len(tests)) for i, test := range tests { _, e := btcjson.GenerateHelp(test.method, nil, test.resultTypes..., ) if reflect.TypeOf(e) != reflect.TypeOf(test.e) { t.Errorf("Test #%d (%s) wrong error - got %T (%v), "+ "want %T", i, test.name, e, e, test.e, ) continue } gotErrorCode := e.(btcjson.GeneralError).ErrorCode if gotErrorCode != test.e.ErrorCode { t.Errorf("Test #%d (%s) mismatched error code - got "+ "%v (%v), want %v", i, test.name, gotErrorCode, e, test.e.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, e := btcjson.GenerateHelp("help", descs) if e != nil { t.Fatalf("GenerateHelp: unexpected error: %v", e) } wantHelp := "help (\"command\")\n" + "test\n\nArguments:\n1. command (string, optional) test\n" + "Result:\nNothing\n" if help != wantHelp { t.Fatalf("GenerateHelp: unexpected help - got\n%v\nwant\n%v", help, wantHelp, ) } }