-
Notifications
You must be signed in to change notification settings - Fork 748
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
Test for data race conditions in adapters #1756
Changes from 17 commits
7074f58
cbcc293
a4c6c4a
dde3442
8dc32b2
e81eceb
015fc30
e22ffd7
4ce81b9
eb75ee4
87d8461
4e81fcf
77d9a54
e8d3a6a
6be78a1
f96f9e7
1023f30
5f81f1d
4138379
d0e5e70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,8 +7,10 @@ import ( | |
"regexp" | ||
"testing" | ||
|
||
"github.com/mitchellh/copystructure" | ||
"github.com/mxmCherry/openrtb/v15/openrtb2" | ||
"github.com/prebid/prebid-server/adapters" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/yudai/gojsondiff" | ||
"github.com/yudai/gojsondiff/formatter" | ||
|
||
|
@@ -107,24 +109,10 @@ func runSpec(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidd | |
} else if isVideoTest { | ||
reqInfo.PbsEntryPoint = "video" | ||
} | ||
actualReqs, errs := bidder.MakeRequests(&spec.BidRequest, &reqInfo) | ||
diffErrorLists(t, fmt.Sprintf("%s: MakeRequests", filename), errs, spec.MakeRequestErrors) | ||
diffHttpRequestLists(t, filename, actualReqs, spec.HttpCalls) | ||
|
||
bidResponses := make([]*adapters.BidderResponse, 0) | ||
|
||
var bidsErrs = make([]error, 0, len(spec.MakeBidsErrors)) | ||
for i := 0; i < len(actualReqs); i++ { | ||
thisBidResponse, theseErrs := bidder.MakeBids(&spec.BidRequest, spec.HttpCalls[i].Request.ToRequestData(t), spec.HttpCalls[i].Response.ToResponseData(t)) | ||
bidsErrs = append(bidsErrs, theseErrs...) | ||
bidResponses = append(bidResponses, thisBidResponse) | ||
} | ||
|
||
diffErrorLists(t, fmt.Sprintf("%s: MakeBids", filename), bidsErrs, spec.MakeBidsErrors) | ||
requests := testMakeRequestsImpl(t, filename, spec, bidder, &reqInfo) | ||
|
||
for i := 0; i < len(spec.BidResponses); i++ { | ||
diffBidLists(t, filename, bidResponses[i], spec.BidResponses[i].Bids) | ||
} | ||
testMakeBidsImpl(t, filename, spec, bidder, requests) | ||
} | ||
|
||
type testSpec struct { | ||
|
@@ -194,8 +182,8 @@ type expectedBid struct { | |
// | ||
// Marshalling the structs and then using a JSON-diff library isn't great either, since | ||
|
||
// diffHttpRequests compares the actual http requests to the expected ones. | ||
func diffHttpRequestLists(t *testing.T, filename string, actual []*adapters.RequestData, expected []httpCall) { | ||
// assertMakeRequestsOutput compares the actual http requests to the expected ones. | ||
func assertMakeRequestsOutput(t *testing.T, filename string, actual []*adapters.RequestData, expected []httpCall) { | ||
t.Helper() | ||
|
||
if len(expected) != len(actual) { | ||
|
@@ -206,7 +194,7 @@ func diffHttpRequestLists(t *testing.T, filename string, actual []*adapters.Requ | |
} | ||
} | ||
|
||
func diffErrorLists(t *testing.T, description string, actual []error, expected []testSpecExpectedError) { | ||
func assertErrorList(t *testing.T, description string, actual []error, expected []testSpecExpectedError) { | ||
t.Helper() | ||
|
||
if len(expected) != len(actual) { | ||
|
@@ -227,10 +215,10 @@ func diffErrorLists(t *testing.T, description string, actual []error, expected [ | |
} | ||
} | ||
|
||
func diffBidLists(t *testing.T, filename string, response *adapters.BidderResponse, expected []expectedBid) { | ||
func assertMakeBidsOutput(t *testing.T, filename string, bidderResponse *adapters.BidderResponse, expected []expectedBid) { | ||
t.Helper() | ||
|
||
if (response == nil || len(response.Bids) == 0) != (len(expected) == 0) { | ||
if (bidderResponse == nil || len(bidderResponse.Bids) == 0) != (len(expected) == 0) { | ||
if len(expected) == 0 { | ||
t.Fatalf("%s: expectedBidResponses indicated a nil response, but mockResponses supplied a non-nil response", filename) | ||
} | ||
|
@@ -239,17 +227,15 @@ func diffBidLists(t *testing.T, filename string, response *adapters.BidderRespon | |
} | ||
|
||
// Expected nil response - give diffBids something to work with. | ||
if response == nil { | ||
response = new(adapters.BidderResponse) | ||
if bidderResponse == nil { | ||
bidderResponse = new(adapters.BidderResponse) | ||
} | ||
|
||
actual := response.Bids | ||
|
||
if len(actual) != len(expected) { | ||
t.Fatalf("%s: MakeBids returned wrong bid count. Expected %d, got %d", filename, len(expected), len(actual)) | ||
if len(bidderResponse.Bids) != len(expected) { | ||
t.Fatalf("%s: MakeBids returned wrong bid count. Expected %d, got %d", filename, len(expected), len(bidderResponse.Bids)) | ||
} | ||
for i := 0; i < len(actual); i++ { | ||
diffBids(t, fmt.Sprintf("%s: typedBid[%d]", filename, i), actual[i], &(expected[i])) | ||
for i := 0; i < len(bidderResponse.Bids); i++ { | ||
diffBids(t, fmt.Sprintf("%s: typedBid[%d]", filename, i), bidderResponse.Bids[i], &(expected[i])) | ||
} | ||
} | ||
|
||
|
@@ -331,3 +317,84 @@ func diffJson(t *testing.T, description string, actual []byte, expected []byte) | |
} | ||
} | ||
} | ||
|
||
// testMakeRequestsImpl asserts the resulting values of the bidder's `MakeRequests()` implementation | ||
// against the expected JSON-defined results and makes sure we do not incurr in data races in the | ||
// process. To verify no data races occur this function creates: | ||
// 1) A shallow copy of the unmarshalled openrtb2.BidRequest that will provide reference values to | ||
// shared memory that we don't want the adapters' implementation of `MakeRequests()` to modify. | ||
// 2) A deep copy that will preserve the original values of all the fields. This copy remains untouched | ||
// by the adapters' processes and serves as reference of what the shared memory values should still | ||
// be after the `MakeRequests()` call. | ||
// | ||
// The original values stored in the deep copy will be compared against the shared memory values and | ||
// discrepancies will point to data race conditions in an adapter's `MakeRequests()` implementation. | ||
// | ||
// Because neither the shallow nor the deep copies are passed to `MakeRequests()` we know static fields | ||
// in openrtb2.BidRequest nor openrtb2.Imp elements will change, therefore, we can reliably compare | ||
// reference values using the `assert.Equal()` method. | ||
func testMakeRequestsImpl(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, reqInfo *adapters.ExtraRequestInfo) []*adapters.RequestData { | ||
t.Helper() | ||
|
||
deepBidReqCopy, shallowBidReqCopy, err := getDataRaceTestCopies(&spec.BidRequest) | ||
assert.NoError(t, err, "Could not create deep copy for data race assertions. %s\n", filename) | ||
SyntaxNode marked this conversation as resolved.
Show resolved
Hide resolved
SyntaxNode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Run MakeRequests | ||
requests, errs := bidder.MakeRequests(&spec.BidRequest, reqInfo) | ||
|
||
// Compare MakeRequests actual output versus expected values found in JSON file | ||
assertErrorList(t, fmt.Sprintf("%s: MakeRequests", filename), errs, spec.MakeRequestErrors) | ||
assertMakeRequestsOutput(t, filename, requests, spec.HttpCalls) | ||
|
||
// Assert no data races occur using original bidRequest copies of references and values | ||
assert.Equal(t, deepBidReqCopy, shallowBidReqCopy, "Data race found. Test: %s \n", filename) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After many local tests, local adapter modifications to purposefully create race conditions, and trying out many deep copy libraries available out there: we finally found the mix to simply I still think it's worth it though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine. Testisy will highlight the differences between the expected and actual. |
||
|
||
return requests | ||
} | ||
|
||
// getDataRaceTestCopies returns a deep copy and a shallow copy of the original bidRequest that will get | ||
// compared to verify no data races occur. | ||
func getDataRaceTestCopies(original *openrtb2.BidRequest) (*openrtb2.BidRequest, *openrtb2.BidRequest, error) { | ||
cpy, err := copystructure.Copy(original) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
deepReqCopy := cpy.(*openrtb2.BidRequest) | ||
|
||
shallowReqCopy := *original | ||
|
||
// If we call `copy` on a nil []Imp, the shallowReqCopy.Imp slice will initialize to openrtb2.Imp{} | ||
// and not nil, which will lead to false positives. | ||
if original.Imp != nil { | ||
SyntaxNode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
shallowReqCopy.Imp = make([]openrtb2.Imp, len(original.Imp)) | ||
copy(shallowReqCopy.Imp, original.Imp) | ||
} | ||
|
||
return deepReqCopy, &shallowReqCopy, nil | ||
} | ||
|
||
// testMakeBidsImpl asserts the results of the bidder MakeBids implementation against the expected JSON-defined results | ||
func testMakeBidsImpl(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, makeRequestsOut []*adapters.RequestData) { | ||
t.Helper() | ||
|
||
bidResponses := make([]*adapters.BidderResponse, 0) | ||
var bidsErrs = make([]error, 0, len(spec.MakeBidsErrors)) | ||
|
||
// We should have as many bids as number of adapters.RequestData found in MakeRequests output | ||
for i := 0; i < len(makeRequestsOut); i++ { | ||
// Run MakeBids with JSON refined spec.HttpCalls info that was asserted to match MakeRequests | ||
// output inside testMakeRequestsImpl | ||
thisBidResponse, theseErrs := bidder.MakeBids(&spec.BidRequest, spec.HttpCalls[i].Request.ToRequestData(t), spec.HttpCalls[i].Response.ToResponseData(t)) | ||
|
||
bidsErrs = append(bidsErrs, theseErrs...) | ||
bidResponses = append(bidResponses, thisBidResponse) | ||
} | ||
|
||
// Assert actual errors thrown by MakeBids implementation versus expected JSON-defined spec.MakeBidsErrors | ||
assertErrorList(t, fmt.Sprintf("%s: MakeBids", filename), bidsErrs, spec.MakeBidsErrors) | ||
|
||
// Assert MakeBids implementation BidResponses with expected JSON-defined spec.BidResponses[i].Bids | ||
for i := 0; i < len(spec.BidResponses); i++ { | ||
assertMakeBidsOutput(t, filename, bidResponses[i], spec.BidResponses[i].Bids) | ||
} | ||
} | ||
SyntaxNode marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't end up liking this huge comment. It probably needs rewording. Suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO the last two paragraphs can be removed. One other suggestion: