adds tests

This commit is contained in:
Josh Humphries 2018-10-16 12:19:44 -04:00
parent d76351d11b
commit c7a5192cf6
9 changed files with 494 additions and 64 deletions

View File

@ -0,0 +1,303 @@
package main
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/struct"
"github.com/jhump/protoreflect/desc"
"google.golang.org/grpc/metadata"
"github.com/fullstorydev/grpcurl"
)
func TestRequestFactory(t *testing.T) {
source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset")
if err != nil {
t.Fatalf("failed to create descriptor source: %v", err)
}
msg, err := makeProto()
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
testCases := []struct {
format string
input string
expectedOutput []proto.Message
}{
{
format: "json",
input: "",
},
{
format: "json",
input: messageAsJSON,
expectedOutput: []proto.Message{msg},
},
{
format: "json",
input: messageAsJSON + messageAsJSON + messageAsJSON,
expectedOutput: []proto.Message{msg, msg, msg},
},
{
// unlike JSON, empty input yields one empty message (vs. zero messages)
format: "text",
input: "",
expectedOutput: []proto.Message{&structpb.Value{}},
},
{
format: "text",
input: messageAsText,
expectedOutput: []proto.Message{msg},
},
{
format: "text",
input: messageAsText + string(textSeparatorChar),
expectedOutput: []proto.Message{msg, &structpb.Value{}},
},
{
format: "text",
input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText,
expectedOutput: []proto.Message{msg, msg, msg},
},
}
for i, tc := range testCases {
name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput))
rf, _ := formatDetails(tc.format, source, false, strings.NewReader(tc.input))
numReqs := 0
for {
var req structpb.Value
err := rf.next(&req)
if err == io.EOF {
break
} else if err != nil {
t.Errorf("%s, msg %d: unexpected error: %v", name, numReqs, err)
}
if !proto.Equal(&req, tc.expectedOutput[numReqs]) {
t.Errorf("%s, msg %d: incorrect message;\nexpecting:\n%v\ngot:\n%v", name, numReqs, tc.expectedOutput[numReqs], &req)
}
numReqs++
}
if rf.numRequests() != numReqs {
t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.numRequests())
}
}
}
// Handler prints response data (and headers/trailers in verbose mode).
// This verifies that we get the right output in both JSON and proto text modes.
func TestHandler(t *testing.T) {
source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset")
if err != nil {
t.Fatalf("failed to create descriptor source: %v", err)
}
d, err := source.FindSymbol("TestService.GetFiles")
if err != nil {
t.Fatalf("failed to find method 'TestService.GetFiles': %v", err)
}
md, ok := d.(*desc.MethodDescriptor)
if !ok {
t.Fatalf("wrong kind of descriptor found: %T", d)
}
reqHeaders := metadata.Pairs("foo", "123", "bar", "456")
respHeaders := metadata.Pairs("foo", "abc", "bar", "def", "baz", "xyz")
respTrailers := metadata.Pairs("a", "1", "b", "2", "c", "3")
rsp, err := makeProto()
if err != nil {
t.Fatalf("failed to create response message: %v", err)
}
for _, format := range []string{"json", "text"} {
for _, numMessages := range []int{1, 3} {
for _, verbose := range []bool{true, false} {
name := fmt.Sprintf("%s, %d message(s)", format, numMessages)
if verbose {
name += ", verbose"
}
_, formatter := formatDetails(format, source, verbose, nil)
var buf bytes.Buffer
h := handler{
out: &buf,
descSource: source,
verbose: verbose,
formatter: formatter,
}
h.OnResolveMethod(md)
h.OnSendHeaders(reqHeaders)
h.OnReceiveHeaders(respHeaders)
for i := 0; i < numMessages; i++ {
h.OnReceiveResponse(rsp)
}
h.OnReceiveTrailers(nil, respTrailers)
expectedOutput := ""
if verbose {
expectedOutput += verbosePrefix
}
for i := 0; i < numMessages; i++ {
if verbose {
expectedOutput += verboseResponseHeader
}
if format == "json" {
expectedOutput += messageAsJSON
} else {
if i > 0 && !verbose {
expectedOutput += string(textSeparatorChar)
}
expectedOutput += messageAsText
}
}
if verbose {
expectedOutput += verboseSuffix
}
out := buf.String()
if !compare(out, expectedOutput) {
t.Errorf("%s: Incorrect output.", name) // Expected:\n%s\nGot:\n%s", name, expectedOutput, out)
}
}
}
}
}
// compare checks that actual and expected are equal, returning true if so.
// A simple equality check (==) does not suffice because jsonpb formats
// structpb.Value strangely. So if that formatting gets fixed, we don't
// want this test in grpcurl to suddenly start failing. So we check each
// line and compare the lines after stripping whitespace (which removes
// the jsonpb format anomalies).
func compare(actual, expected string) bool {
actualLines := strings.Split(actual, "\n")
expectedLines := strings.Split(expected, "\n")
if len(actualLines) != len(expectedLines) {
return false
}
for i := 0; i < len(actualLines); i++ {
if strings.TrimSpace(actualLines[i]) != strings.TrimSpace(expectedLines[i]) {
return false
}
}
return true
}
func makeProto() (proto.Message, error) {
var rsp structpb.Value
err := jsonpb.UnmarshalString(`{
"foo": ["abc", "def", "ghi"],
"bar": { "a": 1, "b": 2 },
"baz": true,
"null": null
}`, &rsp)
if err != nil {
return nil, err
}
return &rsp, nil
}
var (
verbosePrefix = `
Resolved method descriptor:
{
"name": "GetFiles",
"inputType": ".TestRequest",
"outputType": ".TestResponse",
"options": {
}
}
Request metadata to send:
bar: 456
foo: 123
Response headers received:
bar: def
baz: xyz
foo: abc
`
verboseSuffix = `
Response trailers received:
a: 1
b: 2
c: 3
`
verboseResponseHeader = `
Response contents:
`
messageAsJSON = `{
"bar": {
"a": 1,
"b": 2
},
"baz": true,
"foo": [
"abc",
"def",
"ghi"
],
"null": null
}
`
messageAsText = `struct_value: <
fields: <
key: "bar"
value: <
struct_value: <
fields: <
key: "a"
value: <
number_value: 1
>
>
fields: <
key: "b"
value: <
number_value: 2
>
>
>
>
>
fields: <
key: "baz"
value: <
bool_value: true
>
>
fields: <
key: "foo"
value: <
list_value: <
values: <
string_value: "abc"
>
values: <
string_value: "def"
>
values: <
string_value: "ghi"
>
>
>
>
fields: <
key: "null"
value: <
null_value: NULL_VALUE
>
>
>
`
)

View File

@ -65,21 +65,22 @@ var (
rpcHeaders multiString rpcHeaders multiString
reflHeaders multiString reflHeaders multiString
authority = flag.String("authority", "", authority = flag.String("authority", "",
":authority pseudo header value to be passed along with underlying HTTP/2 requests. It defaults to `host [ \":\" port ]` part of the target url.") `:authority pseudo header value to be passed along with underlying HTTP/2
requests. It defaults to 'host [ ":" port ]' part of the target url.`)
data = flag.String("d", "", data = flag.String("d", "",
`Data for request contents. If the value is '@' then the request contents `Data for request contents. If the value is '@' then the request contents
are read from stdin. For calls that accept a stream of requests, the are read from stdin. For calls that accept a stream of requests, the
contents should include all such request messages concatenated together contents should include all such request messages concatenated together
(optionally separated by whitespace).`) (optionally separated by whitespace).`)
format = flag.String("format", "", format = flag.String("format", "json",
`The format of request data. The allowed values are 'json' (the default) `The format of request data. The allowed values are 'json' or 'text'. For
or 'text'. For 'json', the input data must be in JSON format. Multiple 'json', the input data must be in JSON format. Multiple request values may
request values may be concatenated (messages with a JSON representation be concatenated (messages with a JSON representation other than object
other than Object must be separated by whitespace, such as a newline). For must be separated by whitespace, such as a newline). For 'text', the input
'text', the input data must be in the protobuf text format, in which case data must be in the protobuf text format, in which case multiple request
multiple request values must be separated by the "record separate" ASCII values must be separated by the "record separate" ASCII character: 0x1E.
character: 0x1E. The stream should not end in a record separator. If it The stream should not end in a record separator. If it does, it will be
does, it will be interpreted as a final, blank message after the separator.`) interpreted as a final, blank message after the separator.`)
connectTimeout = flag.String("connect-timeout", "", connectTimeout = flag.String("connect-timeout", "",
`The maximum time, in seconds, to wait for connection to be established. `The maximum time, in seconds, to wait for connection to be established.
Defaults to 10 seconds.`) Defaults to 10 seconds.`)
@ -180,7 +181,7 @@ func main() {
if (*key == "") != (*cert == "") { if (*key == "") != (*cert == "") {
fail(nil, "The -cert and -key arguments must be used together and both be present.") fail(nil, "The -cert and -key arguments must be used together and both be present.")
} }
if *format != "" && *format != "json" && *format != "text" { if *format != "json" && *format != "text" {
fail(nil, "The -format option must be 'json' or 'text.") fail(nil, "The -format option must be 'json' or 'text.")
} }
@ -432,7 +433,7 @@ func main() {
// create a request to invoke an RPC // create a request to invoke an RPC
tmpl := makeTemplate(dynamic.NewMessage(dsc)) tmpl := makeTemplate(dynamic.NewMessage(dsc))
fmt.Println("\nMessage template:") fmt.Println("\nMessage template:")
if *format == "" || *format == "json" { if *format == "json" {
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true} jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
err := jsm.Marshal(os.Stdout, tmpl) err := jsm.Marshal(os.Stdout, tmpl)
if err != nil { if err != nil {
@ -460,24 +461,12 @@ func main() {
in = strings.NewReader(*data) in = strings.NewReader(*data)
} }
var rf requestFactory rf, formatter := formatDetails(*format, descSource, *verbose, in)
var h handler h := handler{
if *format == "" || *format == "json" { out: os.Stdout,
resolver, err := anyResolver(descSource)
if err != nil {
fail(err, "Error creating message resolver")
}
rf = newJsonFactory(in, resolver)
h = handler{
descSource: descSource, descSource: descSource,
marshaler: jsonpb.Marshaler{ formatter: formatter,
EmitDefaults: *emitDefaults, verbose: *verbose,
AnyResolver: resolver,
},
}
} else {
rf = newTextFactory(in)
h = handler{descSource: descSource}
} }
err := grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), &h, rf.next) err := grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), &h, rf.next)
@ -568,63 +557,74 @@ func anyResolver(source grpcurl.DescriptorSource) (jsonpb.AnyResolver, error) {
return dynamic.AnyResolver(mf, files...), nil return dynamic.AnyResolver(mf, files...), nil
} }
func formatDetails(format string, descSource grpcurl.DescriptorSource, verbose bool, in io.Reader) (requestFactory, func(proto.Message) (string, error)) {
if format == "json" {
resolver, err := anyResolver(descSource)
if err != nil {
fail(err, "Error creating message resolver")
}
marshaler := jsonpb.Marshaler{
EmitDefaults: *emitDefaults,
Indent: " ",
AnyResolver: resolver,
}
return newJsonFactory(in, resolver), marshaler.MarshalToString
}
/* else *format == "text" */
// if not verbose output, then also include record delimiters
// before each message (other than the first) so output could
// potentially piped to another grpcurl process
tf := textFormatter{useSeparator: !verbose}
return newTextFactory(in), tf.format
}
type handler struct { type handler struct {
out io.Writer
descSource grpcurl.DescriptorSource descSource grpcurl.DescriptorSource
respCount int respCount int
stat *status.Status stat *status.Status
marshaler jsonpb.Marshaler formatter func(proto.Message) (string, error)
verbose bool
} }
func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) { func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) {
if *verbose { if h.verbose {
txt, err := grpcurl.GetDescriptorText(md, h.descSource) txt, err := grpcurl.GetDescriptorText(md, h.descSource)
if err == nil { if err == nil {
fmt.Printf("\nResolved method descriptor:\n%s\n", txt) fmt.Fprintf(h.out, "\nResolved method descriptor:\n%s\n", txt)
} }
} }
} }
func (*handler) OnSendHeaders(md metadata.MD) { func (h *handler) OnSendHeaders(md metadata.MD) {
if *verbose { if h.verbose {
fmt.Printf("\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md)) fmt.Fprintf(h.out, "\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md))
} }
} }
func (*handler) OnReceiveHeaders(md metadata.MD) { func (h *handler) OnReceiveHeaders(md metadata.MD) {
if *verbose { if h.verbose {
fmt.Printf("\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md)) fmt.Fprintf(h.out, "\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md))
} }
} }
const rs = string(0x1e)
func (h *handler) OnReceiveResponse(resp proto.Message) { func (h *handler) OnReceiveResponse(resp proto.Message) {
h.respCount++ h.respCount++
if *verbose { if h.verbose {
fmt.Print("\nResponse contents:\n") fmt.Fprint(h.out, "\nResponse contents:\n")
}
var respStr string
var err error
if *format == "" || *format == "json" {
respStr, err = h.marshaler.MarshalToString(resp)
} else /* *format == "text" */ {
respStr = proto.MarshalTextString(resp)
if !*verbose {
// if not verbose output, then also include record delimiters,
// so output could potentially piped to another grpcurl process
respStr = respStr + rs
}
} }
respStr, err := h.formatter(resp)
if err != nil { if err != nil {
fail(err, "failed to generate JSON form of response message") fail(err, "failed to generate JSON form of response message")
} }
fmt.Println(respStr) fmt.Fprintln(h.out, respStr)
} }
func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
h.stat = stat h.stat = stat
if *verbose { if h.verbose {
fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md)) fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md))
} }
} }
@ -720,6 +720,10 @@ func (f *jsonFactory) numRequests() int {
return f.requestCount return f.requestCount
} }
const (
textSeparatorChar = 0x1e
)
type textFactory struct { type textFactory struct {
r *bufio.Reader r *bufio.Reader
err error err error
@ -736,18 +740,47 @@ func (f *textFactory) next(m proto.Message) error {
} }
var b []byte var b []byte
b, f.err = f.r.ReadBytes(0x1e) b, f.err = f.r.ReadBytes(textSeparatorChar)
if f.err != nil && f.err != io.EOF { if f.err != nil && f.err != io.EOF {
return f.err return f.err
} }
// remove delimiter // remove delimiter
if b[len(b)-1] == 0x1e { if len(b) > 0 && b[len(b)-1] == textSeparatorChar {
b = b[:len(b)-1] b = b[:len(b)-1]
} }
f.requestCount++
return proto.UnmarshalText(string(b), m) return proto.UnmarshalText(string(b), m)
} }
func (f *textFactory) numRequests() int { func (f *textFactory) numRequests() int {
return f.requestCount return f.requestCount
} }
type textFormatter struct {
useSeparator bool
numFormatted int
}
func (tf *textFormatter) format(m proto.Message) (string, error) {
var buf bytes.Buffer
if tf.useSeparator && tf.numFormatted > 0 {
if err := buf.WriteByte(textSeparatorChar); err != nil {
return "", err
}
}
if err := proto.MarshalText(&buf, m); err != nil {
return "", err
}
// no trailing newline needed
str := buf.String()
if str[len(str)-1] == '\n' {
str = str[:len(str)-1]
}
tf.numFormatted++
return str, nil
}

View File

@ -783,16 +783,29 @@ func MetadataToString(md metadata.MD) string {
if len(md) == 0 { if len(md) == 0 {
return "(empty)" return "(empty)"
} }
keys := make([]string, 0, len(md))
for k := range md {
keys = append(keys, k)
}
sort.Strings(keys)
var b bytes.Buffer var b bytes.Buffer
for k, vs := range md { first := true
for _, k := range keys {
vs := md[k]
for _, v := range vs { for _, v := range vs {
if first {
first = false
} else {
b.WriteString("\n")
}
b.WriteString(k) b.WriteString(k)
b.WriteString(": ") b.WriteString(": ")
if strings.HasSuffix(k, "-bin") { if strings.HasSuffix(k, "-bin") {
v = base64.StdEncoding.EncodeToString([]byte(v)) v = base64.StdEncoding.EncodeToString([]byte(v))
} }
b.WriteString(v) b.WriteString(v)
b.WriteString("\n")
} }
} }
return b.String() return b.String()

View File

@ -44,6 +44,10 @@ type descSourceCase struct {
includeRefl bool includeRefl bool
} }
// NB: These tests intentionally use the deprecated InvokeRpc since that
// calls the other (non-deprecated InvokeRPC). That allows the tests to
// easily exercise both functions.
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
var err error var err error
sourceProtoset, err = DescriptorSourceFromProtoSets("testing/test.protoset") sourceProtoset, err = DescriptorSourceFromProtoSets("testing/test.protoset")
@ -235,6 +239,73 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
} }
} }
func TestGetAllFiles(t *testing.T) {
expectedFiles := []string{"testing/test.proto"}
// server reflection picks up filename from linked in Go package,
// which indicates "grpc_testing/test.proto", not our local copy.
expectedFilesWithReflection := []string{"grpc_reflection_v1alpha/reflection.proto", "grpc_testing/test.proto"}
for _, ds := range descSources {
t.Run(ds.name, func(t *testing.T) {
files, err := GetAllFiles(ds.source)
if err != nil {
t.Fatalf("failed to get all files: %v", err)
}
names := fileNames(files)
expected := expectedFiles
if ds.includeRefl {
expected = expectedFilesWithReflection
}
if !reflect.DeepEqual(expected, names) {
t.Errorf("GetAllFiles returned wrong results: wanted %v, got %v", expected, names)
}
})
}
// try cases with more complicated set of files
otherSourceProtoset, err := DescriptorSourceFromProtoSets("testing/test.protoset", "testing/example.protoset")
if err != nil {
t.Fatal(err.Error())
}
otherSourceProtoFiles, err := DescriptorSourceFromProtoFiles(nil, "testing/test.proto", "testing/example.proto")
if err != nil {
t.Fatal(err.Error())
}
otherDescSources := []descSourceCase{
{"protoset[b]", otherSourceProtoset, false},
{"proto[b]", otherSourceProtoFiles, false},
}
expectedFiles = []string{
"google/protobuf/any.proto",
"google/protobuf/descriptor.proto",
"google/protobuf/empty.proto",
"google/protobuf/timestamp.proto",
"testing/example.proto",
"testing/example2.proto",
"testing/test.proto",
}
for _, ds := range otherDescSources {
t.Run(ds.name, func(t *testing.T) {
files, err := GetAllFiles(ds.source)
if err != nil {
t.Fatalf("failed to get all files: %v", err)
}
names := fileNames(files)
if !reflect.DeepEqual(expectedFiles, names) {
t.Errorf("GetAllFiles returned wrong results: wanted %v, got %v", expectedFiles, names)
}
})
}
}
func fileNames(files []*desc.FileDescriptor) []string {
names := make([]string, len(files))
for i, f := range files {
names[i] = f.GetName()
}
return names
}
func TestDescribe(t *testing.T) { func TestDescribe(t *testing.T) {
for _, ds := range descSources { for _, ds := range descSources {
t.Run(ds.name, func(t *testing.T) { t.Run(ds.name, func(t *testing.T) {

View File

@ -7,8 +7,8 @@ cd "$(dirname $0)"
# Run this script to generate files used by tests. # Run this script to generate files used by tests.
echo "Creating protosets..." echo "Creating protosets..."
protoc ../../../google.golang.org/grpc/interop/grpc_testing/test.proto \ protoc testing/test.proto \
-I../../../ --include_imports \ --include_imports \
--descriptor_set_out=testing/test.protoset --descriptor_set_out=testing/test.protoset
protoc testing/example.proto \ protoc testing/example.proto \

View File

@ -3,9 +3,11 @@ syntax = "proto3";
import "google/protobuf/descriptor.proto"; import "google/protobuf/descriptor.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "testing/example2.proto";
message TestRequest { message TestRequest {
repeated string file_names = 1; repeated string file_names = 1;
repeated Extension extensions = 2;
} }
message TestResponse { message TestResponse {

Binary file not shown.

8
testing/example2.proto Normal file
View File

@ -0,0 +1,8 @@
syntax = "proto3";
import "google/protobuf/any.proto";
message Extension {
uint64 id = 1;
google.protobuf.Any data = 2;
}

Binary file not shown.