Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax expectation for unspecified errors, fix expectation for cardinality violations #833

Merged
merged 7 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ runconformance: runservertests runclienttests
.PHONY: runservertests
runservertests: $(BIN)/connectconformance $(BIN)/referenceserver $(BIN)/grpcserver
$(BIN)/connectconformance -v --conf ./testing/reference-impls-config.yaml --mode server --trace \
--known-failing @./testing/referenceserver-known-failing.txt \
-- $(BIN)/referenceserver
$(BIN)/connectconformance -v --conf ./testing/grpc-impls-config.yaml --mode server --trace \
--known-failing @./testing/grpcserver-known-failing.txt \
-- $(BIN)/grpcserver
$(BIN)/connectconformance -v --conf ./testing/grpc-web-server-impl-config.yaml --mode server --trace \
--known-failing @./testing/grpcserver-web-known-failing.txt \
Expand All @@ -106,6 +108,7 @@ runservertests: $(BIN)/connectconformance $(BIN)/referenceserver $(BIN)/grpcserv
.PHONY: runclienttests
runclienttests: $(BIN)/connectconformance $(BIN)/referenceclient $(BIN)/grpcclient buildgrpcweb
$(BIN)/connectconformance -v --conf ./testing/reference-impls-config.yaml --mode client --trace \
--known-failing @./testing/referenceclient-known-failing.txt \
-- $(BIN)/referenceclient
$(BIN)/connectconformance -v --conf ./testing/grpc-impls-config.yaml --mode client --trace \
--known-failing @./testing/grpcclient-known-failing.txt \
Expand Down
53 changes: 34 additions & 19 deletions docs/authoring_test_cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ or must be specified together. If they are omitted, the runner will auto-populat
>
> If a test is specific to one of the first four fields, it should instead be indicated in the directives for the test suite itself.

### Expected responses

The expected response for a test, in the `expectedResponse` field, can be auto-generated based on the request details.
The conformance runner will determine what the response should be according to the values specified in the individual
test case requests.

You also have the ability to explicitly specify your own expected response directly in the test definition. However,
this is typically only needed for exception test cases. If the expected response is mostly re-stating the response
definition that appears in the requests, you should rely on the auto-generation if possible. Otherwise, specifying
an expected response can make the test YAML overly verbose and harder to read, write, and maintain.

If the test induces behavior that prevents the server from sending or client from receiving the full response
definition, it will be necessary to define the expected response explicitly. Timeouts, cancellations, and exceeding
message size limits are good examples of this.

If you do need to specify an explicit response, simply define an `expectedResponse` block for your test case and
this will override the auto-generated expected response in the test runner.

To see tests denoting an explicit response, search the [test suites][test-suite-dir] directory for the word `expectedResponse`.

#### Lenience in Expected Error Codes

There are some cases where a condition is obviously an error, based on the protocol specification, but that
specification does not actually indicate how a client or server should behave in the face of that error. One
example is if a gRPC server receives an HTTP/2 request that has an unexpected content-type or an HTTP method
other than POST.

For these cases, the test author should put the most sensible error code as the expected error code in the
`expectedResponse` field of the test case, but then can also add other acceptable error codes in the
`otherAllowedErrorCodes` field. Even when the error code is not specified, it is not good practice to allow
_any_ error code since many error codes will obviously not apply, and some error codes should never be used
by a client or server implementation library but are reserved for use by actual application logic (see the
table at the bottom of the [gRPC status codes documentation](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)).

### Raw Payloads

There are two message types in the test case schema worth noting here - [`RawHTTPRequest`][raw-http-request] and
Expand Down Expand Up @@ -169,25 +203,6 @@ for a specific protocol or only running the unary tests within a suite.

Aside from the `/` for separating elements on the name, test case names should use `kebab-case` convention.

## Expected responses

The expected response for a test is auto-generated based on the request details. The conformance runner will determine
what the response should be according to the values specified in the test suite and individual test cases.

You also have the ability to explicitly specify your own expected response directly in the test definition itself. However,
this is typically only needed for exception test cases. If the expected response is mostly re-stating the response definition
that appears in the requests, you should rely on the auto-generation if possible. Otherwise, specifying an expected response
can make the test YAML overly verbose and harder to read, write, and maintain.

If the test induces behavior that prevents the server from sending or client from receiving the full response definition, it
will be necessary to define the expected response explicitly. Timeouts, cancellations, and exceeding message size limits are
good examples of this.

If you do need to specify an explicit response, simply define an `expectedResponse` block for your test case and this will
override the auto-generated expected response in the test runner.

To see tests denoting an explicit response, search the [test suites][test-suite-dir] directory for the word `expectedResponse`.

## Running and Debugging New Tests

To test new test cases, you can use `make runconformance`, to run the reference implementations against the new test
Expand Down
40 changes: 33 additions & 7 deletions internal/app/connectconformance/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (r *testResults) assert(
expected := definition.ExpectedResponse
var errs multiErrors

errs = append(errs, checkError(expected.Error, actual.Error)...)
errs = append(errs, checkError(expected.Error, actual.Error, definition.OtherAllowedErrorCodes)...)
errs = append(errs, checkPayloads(expected.Payloads, actual.Payloads)...)

if len(expected.Payloads) == 0 &&
Expand Down Expand Up @@ -459,7 +459,7 @@ func checkPayloads(expected, actual []*conformancev1.ConformancePayload) multiEr
return errs
}

func checkError(expected, actual *conformancev1.Error) multiErrors {
func checkError(expected, actual *conformancev1.Error, otherCodes []conformancev1.Code) multiErrors {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use a map[string]struct{} or other Set type here to make searching easier?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list of codes is always small, so linear scan seemed fine (likely faster than having to create a map). As far as just simplifying the code (since perf is not a big concern here), I think constructing the map from the repeated proto field would be the same amount of code as the linear search through the slice, so didn't seem like a clear win. Also, the linear search is part of the standard library in Go 1.21, which introduces a new slices package, so we could upgrade in the near future and then remove some of this code.

I can add a comment to the inSlice function about removing it once we update to Go 1.21.

switch {
case expected == nil && actual == nil:
// nothing to do
Expand All @@ -471,13 +471,14 @@ func checkError(expected, actual *conformancev1.Error) multiErrors {
}

var errs multiErrors
if expected.Code != actual.Code {
errs = append(errs, fmt.Errorf("actual error code %d (%s) does not match expected code %d (%s)",
actual.Code, connect.Code(actual.Code).String(), expected.Code, connect.Code(expected.Code).String()))
if expected.Code != actual.Code && !inSlice(actual.Code, otherCodes) {
expectedCodes := expectedCodeString(expected.Code, otherCodes)
errs = append(errs, fmt.Errorf("actual error {code: %d (%s), message: %q} does not match expected code %s",
actual.Code, connect.Code(actual.Code).String(), actual.GetMessage(), expectedCodes))
}
if expected.Message != nil && expected.GetMessage() != actual.GetMessage() {
errs = append(errs, fmt.Errorf("actual error message %q does not match expected message %q",
actual.GetMessage(), expected.GetMessage()))
errs = append(errs, fmt.Errorf("actual error {code: %d (%s), message: %q} does not match expected message %q",
actual.Code, connect.Code(actual.Code).String(), actual.GetMessage(), expected.GetMessage()))
}
if len(expected.Details) != len(actual.Details) {
// TODO: Should this be more lenient? Are we okay with a Connect implementation adding extra
Expand Down Expand Up @@ -530,3 +531,28 @@ func indent(s string) string {
}
return strings.Join(lines, "\n")
}

func inSlice[T comparable](elem T, slice []T) bool {
// TODO: delete this function when this repo is using Go 1.21
// and update call sites to instead use slices.Contains
for _, item := range slice {
if item == elem {
return true
}
}
return false
}

func expectedCodeString(expectedCode conformancev1.Code, otherAllowedCodes []conformancev1.Code) string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

allowedCodes := make([]string, len(otherAllowedCodes)+1)
for i, code := range append([]conformancev1.Code{expectedCode}, otherAllowedCodes...) {
allowedCodes[i] = fmt.Sprintf("%d (%s)", code, connect.Code(code).String())
if i == len(allowedCodes)-1 && i != 0 {
allowedCodes[i] = "or " + allowedCodes[i]
}
}
if len(allowedCodes) < 3 {
return strings.Join(allowedCodes, " ")
}
return strings.Join(allowedCodes, ", ")
}
43 changes: 41 additions & 2 deletions internal/app/connectconformance/results_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package connectconformance

import (
"errors"
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -329,7 +330,7 @@ func TestResults_Assert_ReportsAllErrors(t *testing.T) {
}
}`,
expectedErrors: []string{
"actual error code 11 (out_of_range) does not match expected code 5 (not_found)",
`actual error {code: 11 (out_of_range), message: "foobar"} does not match expected code 5 (not_found)`,
},
},
{
Expand All @@ -347,7 +348,7 @@ func TestResults_Assert_ReportsAllErrors(t *testing.T) {
}
}`,
expectedErrors: []string{
`actual error message "oof!" does not match expected message "foobar"`,
`actual error {code: 5 (not_found), message: "oof!"} does not match expected message "foobar"`,
},
},
{
Expand Down Expand Up @@ -842,6 +843,44 @@ func TestCanonicalizeHeaderVals(t *testing.T) {
}
}

func TestExpectedCodeString(t *testing.T) {
t.Parallel()
testCases := []struct {
expectedCode conformancev1.Code
otherCodes []conformancev1.Code
expectedString string
}{
{
expectedCode: conformancev1.Code_CODE_ABORTED,
expectedString: "10 (aborted)",
},
{
expectedCode: conformancev1.Code_CODE_ABORTED,
otherCodes: []conformancev1.Code{conformancev1.Code_CODE_INTERNAL},
expectedString: "10 (aborted) or 13 (internal)",
},
{
expectedCode: conformancev1.Code_CODE_ABORTED,
otherCodes: []conformancev1.Code{conformancev1.Code_CODE_INTERNAL, conformancev1.Code_CODE_CANCELED},
expectedString: "10 (aborted), 13 (internal), or 1 (canceled)",
},
{
expectedCode: conformancev1.Code_CODE_ABORTED,
otherCodes: []conformancev1.Code{
conformancev1.Code_CODE_INTERNAL, conformancev1.Code_CODE_CANCELED, conformancev1.Code_CODE_ALREADY_EXISTS,
},
expectedString: "10 (aborted), 13 (internal), 1 (canceled), or 6 (already_exists)",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(fmt.Sprintf("%d_other_codes", len(testCase.otherCodes)), func(t *testing.T) {
t.Parallel()
require.Equal(t, testCase.expectedString, expectedCodeString(testCase.expectedCode, testCase.otherCodes))
})
}
}

func makeKnownFailing() *testTrie {
return parsePatterns([]string{"known-to-fail/**"})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,3 @@ testCases:
expectedResponse:
error:
code: CODE_UNKNOWN
- request:
testName: unexpected-content-type
service: connectrpc.conformance.v1.ConformanceService
method: Unary
streamType: STREAM_TYPE_UNARY
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest
responseDefinition:
rawResponse:
headers:
- name: Content-Type
value: ["image/jpeg"]
expectedResponse:
error:
code: CODE_UNKNOWN
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,77 @@ testCases:
error:
# mapped from 412 status code (invalid body ignored)
code: CODE_FAILED_PRECONDITION

- request:
testName: client-stream/ok-but-no-response
streamType: STREAM_TYPE_CLIENT_STREAM
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest
responseDefinition:
rawResponse:
status_code: 200
headers:
- name: content-type
value: [ "application/connect+proto" ]
stream:
items:
- flags: 2
payload:
text: |
{
"code": "out_of_range",
"message": "oops",
}
expectedResponse:
error:
code: CODE_UNIMPLEMENTED
- request:
testName: client-stream/multiple-responses
streamType: STREAM_TYPE_CLIENT_STREAM
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest
responseDefinition:
rawResponse:
status_code: 200
headers:
- name: content-type
value: [ "application/connect+proto" ]
stream:
items:
- flags: 0
payload:
binary_message:
"@type": "type.googleapis.com/connectrpc.conformance.v1.ClientStreamResponse"
- flags: 0
payload:
binary_message:
"@type": "type.googleapis.com/connectrpc.conformance.v1.ClientStreamResponse"
- flags: 2
payload:
text: |
{
"code": "out_of_range",
"message": "oops",
}
expectedResponse:
error:
code: CODE_UNIMPLEMENTED

- request:
testName: unexpected-content-type
service: connectrpc.conformance.v1.ConformanceService
method: Unary
streamType: STREAM_TYPE_UNARY
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest
responseDefinition:
rawResponse:
headers:
- name: Content-Type
value: ["image/jpeg"]
expectedResponse:
error:
code: CODE_UNKNOWN
- request:
testName: unexpected-codec
streamType: STREAM_TYPE_UNARY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,45 @@ relevantCompressions:
relevantCodecs:
- CODEC_PROTO
testCases:
- request:
testName: server-stream/no-request
streamType: STREAM_TYPE_SERVER_STREAM
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest
rawRequest:
verb: POST
uri: /connectrpc.conformance.v1.ConformanceService/ServerStream
headers:
- name: content-type
value: [ "application/connect+proto" ]
expectedResponse:
error:
code: CODE_UNIMPLEMENTED
- request:
testName: server-stream/multiple-requests
streamType: STREAM_TYPE_SERVER_STREAM
requestMessages:
- "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest
rawRequest:
verb: POST
uri: /connectrpc.conformance.v1.ConformanceService/ServerStream
headers:
- name: content-type
value: [ "application/connect+proto" ]
stream:
items:
- flags: 0
payload:
binary_message:
"@type": "type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest"
- flags: 0
payload:
binary_message:
"@type": "type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest"
expectedResponse:
error:
code: CODE_UNIMPLEMENTED

- request:
testName: unexpected-verb
streamType: STREAM_TYPE_UNARY
Expand Down
Loading
Loading