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
reflHeaders multiString
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 for request contents. If the value is '@' then the request contents
are read from stdin. For calls that accept a stream of requests, the
contents should include all such request messages concatenated together
(optionally separated by whitespace).`)
format = flag.String("format", "",
`The format of request data. The allowed values are 'json' (the default)
or 'text'. For 'json', the input data must be in JSON format. Multiple
request values may be concatenated (messages with a JSON representation
other than Object must be separated by whitespace, such as a newline). For
'text', the input data must be in the protobuf text format, in which case
multiple request values must be separated by the "record separate" ASCII
character: 0x1E. The stream should not end in a record separator. If it
does, it will be interpreted as a final, blank message after the separator.`)
format = flag.String("format", "json",
`The format of request data. The allowed values are 'json' or 'text'. For
'json', the input data must be in JSON format. Multiple request values may
be concatenated (messages with a JSON representation other than object
must be separated by whitespace, such as a newline). For 'text', the input
data must be in the protobuf text format, in which case multiple request
values must be separated by the "record separate" ASCII character: 0x1E.
The stream should not end in a record separator. If it does, it will be
interpreted as a final, blank message after the separator.`)
connectTimeout = flag.String("connect-timeout", "",
`The maximum time, in seconds, to wait for connection to be established.
Defaults to 10 seconds.`)
@ -180,7 +181,7 @@ func main() {
if (*key == "") != (*cert == "") {
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.")
}
@ -432,7 +433,7 @@ func main() {
// create a request to invoke an RPC
tmpl := makeTemplate(dynamic.NewMessage(dsc))
fmt.Println("\nMessage template:")
if *format == "" || *format == "json" {
if *format == "json" {
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
err := jsm.Marshal(os.Stdout, tmpl)
if err != nil {
@ -460,24 +461,12 @@ func main() {
in = strings.NewReader(*data)
}
var rf requestFactory
var h handler
if *format == "" || *format == "json" {
resolver, err := anyResolver(descSource)
if err != nil {
fail(err, "Error creating message resolver")
}
rf = newJsonFactory(in, resolver)
h = handler{
descSource: descSource,
marshaler: jsonpb.Marshaler{
EmitDefaults: *emitDefaults,
AnyResolver: resolver,
},
}
} else {
rf = newTextFactory(in)
h = handler{descSource: descSource}
rf, formatter := formatDetails(*format, descSource, *verbose, in)
h := handler{
out: os.Stdout,
descSource: descSource,
formatter: formatter,
verbose: *verbose,
}
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
}
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 {
out io.Writer
descSource grpcurl.DescriptorSource
respCount int
stat *status.Status
marshaler jsonpb.Marshaler
formatter func(proto.Message) (string, error)
verbose bool
}
func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) {
if *verbose {
if h.verbose {
txt, err := grpcurl.GetDescriptorText(md, h.descSource)
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) {
if *verbose {
fmt.Printf("\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md))
func (h *handler) OnSendHeaders(md metadata.MD) {
if h.verbose {
fmt.Fprintf(h.out, "\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md))
}
}
func (*handler) OnReceiveHeaders(md metadata.MD) {
if *verbose {
fmt.Printf("\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md))
func (h *handler) OnReceiveHeaders(md metadata.MD) {
if h.verbose {
fmt.Fprintf(h.out, "\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md))
}
}
const rs = string(0x1e)
func (h *handler) OnReceiveResponse(resp proto.Message) {
h.respCount++
if *verbose {
fmt.Print("\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
}
if h.verbose {
fmt.Fprint(h.out, "\nResponse contents:\n")
}
respStr, err := h.formatter(resp)
if err != nil {
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) {
h.stat = stat
if *verbose {
fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md))
if h.verbose {
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
}
const (
textSeparatorChar = 0x1e
)
type textFactory struct {
r *bufio.Reader
err error
@ -736,18 +740,47 @@ func (f *textFactory) next(m proto.Message) error {
}
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 {
return f.err
}
// remove delimiter
if b[len(b)-1] == 0x1e {
if len(b) > 0 && b[len(b)-1] == textSeparatorChar {
b = b[:len(b)-1]
}
f.requestCount++
return proto.UnmarshalText(string(b), m)
}
func (f *textFactory) numRequests() int {
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 {
return "(empty)"
}
keys := make([]string, 0, len(md))
for k := range md {
keys = append(keys, k)
}
sort.Strings(keys)
var b bytes.Buffer
for k, vs := range md {
first := true
for _, k := range keys {
vs := md[k]
for _, v := range vs {
if first {
first = false
} else {
b.WriteString("\n")
}
b.WriteString(k)
b.WriteString(": ")
if strings.HasSuffix(k, "-bin") {
v = base64.StdEncoding.EncodeToString([]byte(v))
}
b.WriteString(v)
b.WriteString("\n")
}
}
return b.String()

View File

@ -44,6 +44,10 @@ type descSourceCase struct {
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) {
var err error
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) {
for _, ds := range descSources {
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.
echo "Creating protosets..."
protoc ../../../google.golang.org/grpc/interop/grpc_testing/test.proto \
-I../../../ --include_imports \
protoc testing/test.proto \
--include_imports \
--descriptor_set_out=testing/test.protoset
protoc testing/example.proto \

View File

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