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

Preview Build Failure: Test Case Refactoring proposal [Work in Progress] #3043

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"putItemRequests",
putItemReqFunction,
0,
true
StopStrategy::STOP_ON_FIRST_FAIL
});


Expand Down Expand Up @@ -642,7 +642,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"getItemRequests",
getItemFunc,
3,
false
StopStrategy::STOP_AFTER_ALL_RETRIES
});


Expand Down Expand Up @@ -676,7 +676,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"scanItemRequest",
scanItemReqFunction,
3,
true
StopStrategy::STOP_ON_FIRST_FAIL
});

EXPECT_EQ(retryObj.execute(), true);
Expand Down Expand Up @@ -729,7 +729,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"updateItemRequests",
updateItemFunc,
0,
true
StopStrategy::STOP_AFTER_ALL_RETRIES
});

auto getItemFunc2 = [this, testValueColumnName, crudTestTableName, numItems , &requestIds](
Expand Down Expand Up @@ -811,7 +811,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"getItemRequests2",
getItemFunc2,
3,
false
StopStrategy::STOP_AFTER_ALL_RETRIES
});


Expand Down Expand Up @@ -861,7 +861,7 @@ TEST_F(TableOperationTest, TestCrudOperations2)
"deleteItemRequests",
deleteItemFunc,
0,
true
StopStrategy::STOP_ON_FIRST_FAIL
});


Expand Down Expand Up @@ -905,9 +905,7 @@ TEST_F(TableOperationTest, TestCrudOperations)
putItemResult.get();
}

//after put requests have been made, it's not necessary that put requests may have been processed to completion before get is called
//for a failing read query we attempt it N times before deeming it fail
//we can generalize retry of a request


//now we get the items we were supposed to be putting and make sure
//they were put successfully.
Expand Down
142 changes: 142 additions & 0 deletions tests/aws-cpp-sdk-logs-integration-tests/CloudWatchLogsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,138 @@ namespace
}
};

TEST_F(CloudWatchLogsOperationTest, PutLogEventsTests2)
{

RetryPlanner<> retryObj;


auto putLogsFunc = [this](const Aws::String& id, int numRetriesLeft) -> bool
{
AWS_UNREFERENCED_PARAM(id);
AWS_UNREFERENCED_PARAM(numRetriesLeft);


CreateLogStreamRequest createStreamRequest;
createStreamRequest.WithLogGroupName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_GROUP))
.WithLogStreamName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_STREAM));
auto createStreamOutcome = m_client->CreateLogStream(createStreamRequest);
AWS_EXPECT_SUCCESS(createStreamOutcome);

bool res = createStreamOutcome.IsSuccess();

PutLogEventsRequest putRequest;
putRequest.WithLogGroupName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_GROUP))
.WithLogStreamName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_STREAM));

auto nowMs = Aws::Utils::DateTime::Now().Millis();
InputLogEvent e1;
e1.WithTimestamp(nowMs).WithMessage("Test Message 1");
InputLogEvent e2;
// Make sure the timestamp of e2 is greater than that of e1.
e2.WithTimestamp(nowMs+1).WithMessage("Test Message 2");

putRequest.AddLogEvents(e1).AddLogEvents(e2);
auto putOutcome = m_client->PutLogEvents(putRequest);


AWS_EXPECT_SUCCESS(putOutcome);
const auto& rejected = putOutcome.GetResult().GetRejectedLogEventsInfo();
EXPECT_EQ(rejected.GetExpiredLogEventEndIndex(), 0);
EXPECT_EQ(rejected.GetTooNewLogEventStartIndex(), 0);
EXPECT_EQ(rejected.GetTooOldLogEventEndIndex(), 0);


res = res && putOutcome.IsSuccess();

// The log events in the batch must be in chronological order by their timestamp
nowMs = nowMs + 2;
InputLogEvent e3;
e3.WithTimestamp(nowMs).WithMessage("Test Message 3");
InputLogEvent e4;
// Make sure the timestamp of e4 is greater than that of e3.
e4.WithTimestamp(nowMs+1).WithMessage("Test Message 4");
putRequest.AddLogEvents(e3).AddLogEvents(e4);
putOutcome = m_client->PutLogEvents(putRequest);
AWS_EXPECT_SUCCESS(putOutcome);
const auto& rejected2 = putOutcome.GetResult().GetRejectedLogEventsInfo();
EXPECT_EQ(rejected2.GetExpiredLogEventEndIndex(), 0);
EXPECT_EQ(rejected2.GetTooNewLogEventStartIndex(), 0);
EXPECT_EQ(rejected2.GetTooOldLogEventEndIndex(), 0);

return res && putOutcome.IsSuccess();

};

retryObj.addFunction(RetryPlanner<>::FunctionBlock{
"putLogEvents",
putLogsFunc,
0,
StopStrategy::STOP_ON_FIRST_FAIL
});

size_t eventsCount = 0;
auto getLogsFunc = [this,&eventsCount](const Aws::String& id, int numRetriesLeft) -> bool
{
AWS_UNREFERENCED_PARAM(id);

//There should be in total 6 events in the stream. with messages ended with 1,2,1,2,3,4;
GetLogEventsRequest getRequest;
getRequest.WithLogGroupName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_GROUP))
.WithLogStreamName(BuildResourceName(BASE_CLOUD_WATCH_LOGS_STREAM))
.WithStartFromHead(true);

//This is wher contexual retry can be used and we must only evaluate using gtest macros on last retry on success
//else the failure will be sticky

auto getOutcome = m_client->GetLogEvents(getRequest);

auto res = getOutcome.IsSuccess();

AWS_EXPECT_SUCCESS(getOutcome);
auto outputEvents = getOutcome.GetResult().GetEvents();
eventsCount = outputEvents.size();
res = res && (eventsCount == 6);

Aws::Vector<Aws::String> msgs = {"Test Message 1", "Test Message 1", "Test Message 2",
"Test Message 2","Test Message 3","Test Message 4",};

Aws::String dummy = "N/A";
for (size_t i = 0; i < 6; i++)
{
//This will make the test not crash and actually log valid failure results
auto outputEvent = i < outputEvents.size() ? outputEvents[i].GetMessage() : dummy ;
res = res && (msgs[i] == outputEvent );
}


if (res || numRetriesLeft == SECONDS_TO_WAIT-3)
{
for (size_t i = 0; i < 6; i++)
{
//This will make the test not crash and actually log valid failure results
auto outputEvent = i < outputEvents.size() ? outputEvents[i].GetMessage() : dummy ;
EXPECT_STREQ(msgs[i].c_str(), outputEvent.c_str());
}
}

std::this_thread::sleep_for(std::chrono::seconds(1));

return res;

};

retryObj.addFunction(RetryPlanner<>::FunctionBlock{
"getLogEvents",
getLogsFunc,
SECONDS_TO_WAIT,
StopStrategy::STOP_AFTER_ALL_RETRIES
});

EXPECT_EQ(retryObj.execute(), true);

}

TEST_F(CloudWatchLogsOperationTest, PutLogEventsTests)
{
CreateLogStreamRequest createStreamRequest;
Expand All @@ -117,6 +249,8 @@ namespace

putRequest.AddLogEvents(e1).AddLogEvents(e2);
auto putOutcome = m_client->PutLogEvents(putRequest);


AWS_ASSERT_SUCCESS(putOutcome);
const auto& rejected = putOutcome.GetResult().GetRejectedLogEventsInfo();
EXPECT_EQ(rejected.GetExpiredLogEventEndIndex(), 0);
Expand All @@ -132,6 +266,13 @@ namespace
e4.WithTimestamp(nowMs+1).WithMessage("Test Message 4");
putRequest.AddLogEvents(e3).AddLogEvents(e4);

auto logEvents = putRequest.GetLogEvents();
for(auto le : logEvents)
{
std::cout<<"event:"<<le.GetMessage()<<std::endl;
}


putOutcome = m_client->PutLogEvents(putRequest);
AWS_ASSERT_SUCCESS(putOutcome);
const auto& rejected2 = putOutcome.GetResult().GetRejectedLogEventsInfo();
Expand All @@ -146,6 +287,7 @@ namespace
.WithStartFromHead(true);
size_t eventsCount = 0;
size_t retry = 0;

while (retry < SECONDS_TO_WAIT)
{
auto getOutcome = m_client->GetLogEvents(getRequest);
Expand Down
99 changes: 83 additions & 16 deletions tests/testing-resources/include/aws/testing/AwsTestHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <aws/core/VersionConfig.h>
#include <aws/core/utils/DateTime.h>
#include <aws/core/utils/memory/stl/AWSSet.h>
#include <type_traits>
#include <ostream>

#define AWS_ASSERT_SUCCESS(awsCppSdkOutcome) \
ASSERT_TRUE(awsCppSdkOutcome.IsSuccess()) << "Error details: " << awsCppSdkOutcome.GetError() \
Expand Down Expand Up @@ -78,33 +80,64 @@ OutcomeT CallOperationWithUnconditionalRetry(const ClientT* client,
*/


template<typename CONTEXT>
class RetryPlanner

namespace{
using EmptyClassType = std::nullptr_t;
}

enum StopStrategy{
CONTINUE_AFTER_FAIL,
STOP_ON_FIRST_FAIL,
STOP_AFTER_ALL_RETRIES
};


template<typename CONTEXT = EmptyClassType>
class RetryPlanner
{

public:

using RetryFunction_t = std::function<bool (std::shared_ptr<CONTEXT>, const Aws::String& id, int numRetriesLeft)>;


using CONTEXT_TYPE = std::shared_ptr<CONTEXT>;


//It can work with or without context
using RetryFunction_t = typename std::conditional< std::is_same<CONTEXT, EmptyClassType>::value ,
std::function<bool (const Aws::String& id, int numRetriesLeft)>,
std::function<bool (CONTEXT_TYPE, const Aws::String& id, int numRetriesLeft)>
>::type;

using BlockIdentifierType = Aws::String;



struct FunctionBlock{
Aws::String entryId;
BlockIdentifierType entryId;
RetryFunction_t candidate;
size_t retryCount;
bool stopOnFail;
explicit FunctionBlock(const Aws::String& _entryId,
StopStrategy stopStrategy;
explicit FunctionBlock(const BlockIdentifierType& _entryId,
RetryFunction_t f,
int _retryCount = 2,
bool _stopOnFail = false):
entryId(_entryId), candidate(std::move(f)), retryCount(_retryCount+1), stopOnFail(_stopOnFail)
size_t _retryCount = 2,
StopStrategy _stopStrategy = StopStrategy::CONTINUE_AFTER_FAIL //This ensures break circuit after all retries and failures
):
entryId{_entryId}, candidate{std::move(f)}, retryCount{_retryCount+1},stopStrategy{_stopStrategy}
{

}
friend std::ostream& operator<<(std::ostream& os, FunctionBlock const & block) {
return os << "EntryId=" <<block.entryId << " retryCount="<<block.retryCount<<" stopStrategy="<<block.stopStrategy<<std::endl;
}
~FunctionBlock(){
//std::cout<<"destructor for "<<entryId<<" called"<<std::endl;
}
};

explicit RetryPlanner(std::shared_ptr<CONTEXT> context):_contextSp{context},_cursor{0}{ }
explicit RetryPlanner():_contextSp{nullptr},_cursor{0}{ }

explicit RetryPlanner(CONTEXT_TYPE context):_contextSp{context},_cursor{0}{ }

bool addFunction(const FunctionBlock& item)
{
Expand All @@ -119,7 +152,7 @@ class RetryPlanner
status = true;
}
}
AWS_LOGSTREAM_TRACE(ALLOCATION_TAG, "added function with id=" << item.entryId<<" num tries="<<item.retryCount<<" stop on fail="<<item.stopOnFail<<std::endl );
AWS_LOGSTREAM_TRACE(ALLOCATION_TAG, "added function block=" << item );
//std::cout<<"added function with id=" << item.entryId<<std::endl;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

print to std outs will be removed before final draft

return status;
}
Expand Down Expand Up @@ -163,14 +196,20 @@ class RetryPlanner
bool localResult = false;

//Try and retry a block
do{}
while( it->retryCount && !(localResult = it->candidate(_contextSp, it->entryId, --it->retryCount) ));
do{
AWS_LOGSTREAM_TRACE(ALLOCATION_TAG, "execute block="<<*it );
//std::cout<<"execute block="<<*it<<std::endl;
}
while( it->retryCount &&
!(localResult = invoker<CONTEXT>()(it->candidate, it->entryId, --it->retryCount)) &&
(it->stopStrategy != StopStrategy::STOP_ON_FIRST_FAIL)
);

//set global status from block evaluation
status = status && localResult;

//if stop on fail after retries, break and retry from start if available
if(it->stopOnFail && !localResult)
if(( it->stopStrategy == StopStrategy::STOP_ON_FIRST_FAIL || it->stopStrategy == StopStrategy::STOP_AFTER_ALL_RETRIES ) && !localResult)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a retry plan stop strategy as needed by use case.
One can choose to continue even on failure, stop after first failures or stop after exhausting all retries.

{
break;
}
Expand All @@ -197,13 +236,41 @@ class RetryPlanner
private:
using FunctionSuite = std::list<FunctionBlock>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can use any container, I used generic iterator interface for this reason. Can make this a template parameter with type traits checks. This is just enhancement

FunctionSuite _suite;
std::shared_ptr<CONTEXT> _contextSp;
CONTEXT_TYPE _contextSp;
Aws::UnorderedSet<Aws::String> _uniqueFunctionBlockSet;
size_t _cursor;

static const char ALLOCATION_TAG[];

//compile time conditional invoking of callable function with different arguments depending upon available context or not in c++ 11

using RetryFunctionWrapped = std::function<bool (const RetryFunction_t& callable, const BlockIdentifierType& , size_t) >;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a way for the Retry Planner class to not mandate context where test case doesn't need one. This is implemented using SFINAE concepts and type traits


//the second template parameter is only defined for appropriate condition regarding context type is set or not is true
//Per SFINAE, the case where the 2nd parameter condition is false won't lead to any substitution error but rather no definition
//The second paramter defined is hidden using default value
template <typename T,
typename std::enable_if<!std::is_same<T,EmptyClassType>::value, bool >::type = 0 >
RetryFunctionWrapped invoker()
{

return [this](const RetryFunction_t& callable, const BlockIdentifierType& id , size_t retriesLeft) {
return callable( _contextSp, id, retriesLeft);
};
}

template <typename T,
typename std::enable_if<std::is_same<T,EmptyClassType>::value, bool >::type = 0>
RetryFunctionWrapped invoker()
{
return [](const RetryFunction_t& callable, const BlockIdentifierType& id , size_t retriesLeft) {
return callable(id, retriesLeft);
};
}

};

template <typename CONTEXT>
const char RetryPlanner<CONTEXT>::ALLOCATION_TAG[] = "RetryPlanner";
const char RetryPlanner<CONTEXT>::ALLOCATION_TAG[] = "RetryPlanner";


Loading