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

Test for data race conditions in adapters #1756

Merged
merged 20 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from 10 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
158 changes: 129 additions & 29 deletions adapters/adapterstest/test_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"regexp"
"testing"

"github.com/jinzhu/copier"
"github.com/mxmCherry/openrtb/v14/openrtb2"
"github.com/prebid/prebid-server/adapters"
"github.com/stretchr/testify/assert"
"github.com/yudai/gojsondiff"
"github.com/yudai/gojsondiff/formatter"

Expand Down Expand Up @@ -83,7 +85,14 @@ func loadFile(filename string) (*testSpec, error) {
return nil, fmt.Errorf("Failed to read file %s: %v", filename, err)
}

//var buf bytes.Buffer

//if err := json.Compact(&buf, specData); err != nil {
// return nil, fmt.Errorf("Unable to compact JSON string found in file %s: %v", filename, err)
//}

var spec testSpec
//if err := json.Unmarshal(buf.Bytes(), &spec); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove the commented out code.

if err := json.Unmarshal(specData, &spec); err != nil {
return nil, fmt.Errorf("Failed to unmarshal JSON from file: %v", err)
}
Expand All @@ -107,24 +116,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)
actualReqs := testMakeRequestsImpl(t, filename, spec, bidder, &reqInfo)
Copy link
Contributor

Choose a reason for hiding this comment

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

The actualReqs name seems a little odd to me. Can we just call this reqs or requests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


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)

for i := 0; i < len(spec.BidResponses); i++ {
diffBidLists(t, filename, bidResponses[i], spec.BidResponses[i].Bids)
}
testMakeBidsImpl(t, filename, spec, bidder, actualReqs)
}

type testSpec struct {
Expand Down Expand Up @@ -194,8 +189,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) {
Expand All @@ -206,7 +201,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) {
Expand All @@ -227,10 +222,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, actualBidderResp *adapters.BidderResponse, expected []expectedBid) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this just be bidderResponse? What does the actual part of the name identify?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought the actual part would make it more explicit that those are the values we want to assert. Removed

t.Helper()

if (response == nil || len(response.Bids) == 0) != (len(expected) == 0) {
if (actualBidderResp == nil || len(actualBidderResp.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)
}
Expand All @@ -239,17 +234,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 actualBidderResp == nil {
actualBidderResp = 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(actualBidderResp.Bids) != len(expected) {
t.Fatalf("%s: MakeBids returned wrong bid count. Expected %d, got %d", filename, len(expected), len(actualBidderResp.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(actualBidderResp.Bids); i++ {
diffBids(t, fmt.Sprintf("%s: typedBid[%d]", filename, i), actualBidderResp.Bids[i], &(expected[i]))
}
}

Expand Down Expand Up @@ -331,3 +324,110 @@ func diffJson(t *testing.T, description string, actual []byte, expected []byte)
}
}
}

// testMakeBidsImpl asserts the results of the bidder MakeRequests implementation against the expected JSON-defined results
// and makes sure no data races occur
func testMakeRequestsImpl(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, reqInfo *adapters.ExtraRequestInfo) []*adapters.RequestData {
t.Helper()

deepBidReqCopy, shallowBidReqCopy := getDataRaceTestCopies(&spec.BidRequest)

// Run MakeRequests
actualReqs, errs := bidder.MakeRequests(&spec.BidRequest, reqInfo)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment of regarding actualReqs. What other requests are there in this context?


// Compare MakeRequests actual output versus expected values found in JSON file
assertErrorList(t, fmt.Sprintf("%s: MakeRequests", filename), errs, spec.MakeRequestErrors)
assertMakeRequestsOutput(t, filename, actualReqs, spec.HttpCalls)

// Assert no data races occur using original bidRequest copies of references and values
assertNoDataRace(t, deepBidReqCopy, shallowBidReqCopy, filename)

return actualReqs
}

func getDataRaceTestCopies(original *openrtb2.BidRequest) (*openrtb2.BidRequest, *openrtb2.BidRequest) {

// Save original bidRequest values to assert no data races occur inside MakeRequests latter
deepReqCopy := openrtb2.BidRequest{}
copier.Copy(&deepReqCopy, original)

// Shallow copy PBS core provides to adapters
shallowReqCopy := *original

// Save original []Imp elements to assert no data races occur inside MakeRequests latter
deepReqCopy.Imp = make([]openrtb2.Imp, len(original.Imp))
shallowReqCopy.Imp = make([]openrtb2.Imp, len(original.Imp))
for i := 0; i < len(original.Imp); i++ {
deepImpCopy := openrtb2.Imp{}
copier.Copy(&deepImpCopy, original.Imp[i])
deepReqCopy.Imp = append(deepReqCopy.Imp, deepImpCopy)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I think you explained this before. Why do we need to do make deep copies of the impression objects? Does copier.Copy(&deepReqCopy, original) not already do that?

Copy link
Contributor Author

@guscarreon guscarreon Apr 29, 2021

Choose a reason for hiding this comment

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

Because the deep copy version of this library was throwing false positives when comparing the Ext fields at every level. Switched to github.com/mohae/deepcopy to avoid it. The for loop could not be removed though because we still have to create the shallow copies of the []Imp array elements.


shallowImpCopy := original.Imp[i]
shallowReqCopy.Imp = append(shallowReqCopy.Imp, shallowImpCopy)
}

return &deepReqCopy, &shallowReqCopy
}

// assertNoDataRace compares the contents of the reference fields found in the original openrtb2.BidRequest to their
// original values to make sure they were not modified and we are not incurring indata races. In order to assert
// no data races occur in the []Imp array, we call assertNoImpsDataRace()
func assertNoDataRace(t *testing.T, bidRequestBefore *openrtb2.BidRequest, bidRequestAfter *openrtb2.BidRequest, filename string) {
t.Helper()

// Assert reference fields were not modified by bidder adapter MakeRequests implementation
assert.Equal(t, bidRequestBefore.Site, bidRequestAfter.Site, "Data race in BidRequest.Site field in file %s", filename)
assert.Equal(t, bidRequestBefore.App, bidRequestAfter.App, "Data race in BidRequest.App field in file %s", filename)
assert.Equal(t, bidRequestBefore.Device, bidRequestAfter.Device, "Data race in BidRequest.Device field in file %s", filename)
assert.Equal(t, bidRequestBefore.User, bidRequestAfter.User, "Data race in BidRequest.User field in file %s", filename)
assert.Equal(t, bidRequestBefore.Source, bidRequestAfter.Source, "Data race in BidRequest.Source field in file %s", filename)
assert.Equal(t, bidRequestBefore.Regs, bidRequestAfter.Regs, "Data race in BidRequest.Regs field in file %s", filename)

// Assert Imps separately
assertNoImpsDataRace(t, bidRequestBefore.Imp, bidRequestAfter.Imp, filename)
}

// assertNoImpsDataRace compares the contents of the reference fields found in the original openrtb2.Imp objects to
// their original values to make sure they were not modified and we are not incurring in data races.
func assertNoImpsDataRace(t *testing.T, impsBefore []openrtb2.Imp, impsAfter []openrtb2.Imp, filename string) {
t.Helper()

if assert.Len(t, impsAfter, len(impsBefore), "Original []Imp array was modified and length is not equal to original after MakeRequests was called. File:%s", filename) {
// Assert no data races occured in individual Imp elements
for i := 0; i < len(impsBefore); i++ {
assert.Equal(t, impsBefore[i].Banner, impsAfter[i].Banner, "Data race in bidRequest.Imp[%d].Banner field. File:%s", i, filename)
assert.Equal(t, impsBefore[i].Video, impsAfter[i].Video, "Data race in bidRequest.Imp[%d].Video field. File:%s", i, filename)
assert.Equal(t, impsBefore[i].Audio, impsAfter[i].Audio, "Data race in bidRequest.Imp[%d].Audio field. File:%s", i, filename)
assert.Equal(t, impsBefore[i].Native, impsAfter[i].Native, "Data race in bidRequest.Imp[%d].Native field. File:%s", i, filename)
assert.Equal(t, impsBefore[i].PMP, impsAfter[i].PMP, "Data race in bidRequest.Imp[%d].PMP field. File:%s", i, filename)
assert.Equal(t, impsBefore[i].Secure, impsAfter[i].Secure, "Data race in bidRequest.Imp[%d].Secure field. File:%s", i, filename)
assert.ElementsMatch(t, impsBefore[i].Metric, impsAfter[i].Metric, "Data race in bidRequest.Imp[%d].[]Metric array. File:%s", i)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we include the other slices in the data race tests too?

Copy link
Contributor

Choose a reason for hiding this comment

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

We decided to add all slices from the BidRequest and the Imp using an assert.Equal. Slices are shared memory which also may not be mutated.

}
}
}

// 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/influxdb v1.6.1
github.com/jinzhu/copier v0.2.8
github.com/julienschmidt/httprouter v1.1.0
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
github.com/lib/pq v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/influxdata/influxdb v1.6.1 h1:OseoBlzI5ftNI/bczyxSWq6PKRCNEeiXvyWP/wS5fB0=
github.com/influxdata/influxdb v1.6.1/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
github.com/jinzhu/copier v0.2.8 h1:N8MbL5niMwE3P4dOwurJixz5rMkKfujmMRFmAanSzWE=
github.com/jinzhu/copier v0.2.8/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4EI5MABq68=
github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
Expand Down