Skip to content

Commit

Permalink
Better messages when interaction cannot be fonud
Browse files Browse the repository at this point in the history
  • Loading branch information
Porges committed May 18, 2021
1 parent 6501ba1 commit b4da5b4
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 7 deletions.
100 changes: 100 additions & 0 deletions hack/generated/pkg/testcommon/error_translating_roundtripper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package testcommon

import (
"fmt"
"io"
"net/http"
"strings"

"github.com/dnaeon/go-vcr/cassette"
"github.com/dnaeon/go-vcr/recorder"
"github.com/google/go-cmp/cmp"
)

// translateErrors wraps the given Recorder to handle any "Requested interaction not found"
// and log better information about what the expected request was.
//
// By default the error will be returned to the controller which might ignore/retry it
// and not log any useful information. So instead here we find the recorded request with
// the body that most closely matches what was sent and report the "expected" body.
//
// Ideally we would panic on this error but we don't have a good way to deal with the following
// problem at the moment:
// - during record the controller does GET (404), PUT, … GET (OK)
// - during playback the controller does GET (which now returns OK), DELETE, PUT, …
// and fails due to a missing DELETE recording
func translateErrors(r *recorder.Recorder, cassetteName string) http.RoundTripper {
return errorTranslation{r, cassetteName, nil}
}

type errorTranslation struct {
recorder *recorder.Recorder
cassetteName string

cassette *cassette.Cassette
}

func (w errorTranslation) ensure_cassette() *cassette.Cassette {
if w.cassette == nil {
cassette, err := cassette.Load(w.cassetteName)
if err != nil {
panic(fmt.Sprintf("unable to load casette %q", w.cassetteName))
}

w.cassette = cassette
}

return w.cassette
}

func (w errorTranslation) RoundTrip(req *http.Request) (*http.Response, error) {
resp, originalErr := w.recorder.RoundTrip(req)
// sorry, go-vcr doesn't expose the error type or message
if originalErr == nil || !strings.Contains(originalErr.Error(), "interaction not found") {
return resp, originalErr
}

sentBodyString := "<nil>"
if req.Body != nil {
bodyBytes, bodyErr := io.ReadAll(req.Body)
if bodyErr != nil {
// see invocation of SetMatcher in the createRecorder, which does this
panic("io.ReadAll(req.Body) failed, this should always succeed because req.Body has been replaced by a buffer")
}

sentBodyString = string(bodyBytes)
}

// find all request bodies for the specified method/URL combination
urlString := req.URL.String()
var bodiesForMethodAndURL []string
for _, interaction := range w.ensure_cassette().Interactions {
if urlString == interaction.URL && req.Method == interaction.Request.Method &&
req.Header.Get(COUNT_HEADER) == interaction.Request.Headers.Get(COUNT_HEADER) {
bodiesForMethodAndURL = append(bodiesForMethodAndURL, interaction.Request.Body)
break
}
}

if len(bodiesForMethodAndURL) == 0 {
fmt.Printf("\n*** Cannot find go-vcr recording for request (no responses recorded for this method/URL): %s %s (attempt: %s)\n\n", req.Method, req.URL.String(), req.Header.Get(COUNT_HEADER))
return nil, originalErr
}

// locate the request body with the shortest diff from the sent body
shortestDiff := ""
for i, bodyString := range bodiesForMethodAndURL {
diff := cmp.Diff(sentBodyString, bodyString)
if i == 0 || len(diff) < len(shortestDiff) {
shortestDiff = diff
}
}

fmt.Printf("\n*** Cannot find go-vcr recording for request (body mismatch): %s %s\nShortest body diff: %s\n\n", req.Method, req.URL.String(), shortestDiff)
return nil, originalErr
}
13 changes: 6 additions & 7 deletions hack/generated/pkg/testcommon/test_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package testcommon

import (
"bytes"
"io/ioutil"
"io"
"log"
"net/http"
"strings"
Expand Down Expand Up @@ -56,7 +56,8 @@ func NewTestContext(region string, recordReplay bool) TestContext {
}

func (tc TestContext) ForTest(t *testing.T) (PerTestContext, error) {
authorizer, subscriptionID, recorder, err := createRecorder(t.Name(), tc.RecordReplay)
cassetteName := "recordings/" + t.Name()
authorizer, subscriptionID, recorder, err := createRecorder(cassetteName, tc.RecordReplay)
if err != nil {
return PerTestContext{}, errors.Wrapf(err, "creating recorder")
}
Expand All @@ -68,7 +69,7 @@ func (tc TestContext) ForTest(t *testing.T) (PerTestContext, error) {

// replace the ARM client transport (a bit hacky)
httpClient := armClient.RawClient.Sender.(*http.Client)
httpClient.Transport = recorder
httpClient.Transport = translateErrors(recorder, cassetteName)

t.Cleanup(func() {
if !t.Failed() {
Expand All @@ -92,9 +93,7 @@ func (tc TestContext) ForTest(t *testing.T) (PerTestContext, error) {
}, nil
}

func createRecorder(testName string, recordReplay bool) (autorest.Authorizer, string, *recorder.Recorder, error) {
cassetteName := "recordings/" + testName

func createRecorder(cassetteName string, recordReplay bool) (autorest.Authorizer, string, *recorder.Recorder, error) {
var err error
var r *recorder.Recorder
if recordReplay {
Expand Down Expand Up @@ -134,7 +133,7 @@ func createRecorder(testName string, recordReplay bool) (autorest.Authorizer, st
return false
}

r.Body = ioutil.NopCloser(&b)
r.Body = io.NopCloser(&b)
return cassette.DefaultMatcher(r, i) && (b.String() == "" || b.String() == i.Body)
})

Expand Down

0 comments on commit b4da5b4

Please sign in to comment.