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

Add the ability to wait and retry futures based on multiple expected … #1514

Merged
merged 1 commit into from
Jan 12, 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
35 changes: 25 additions & 10 deletions testing/test_framework/src/firebase_test_framework.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "firebase_test_framework.h" // NOLINT

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
Expand Down Expand Up @@ -126,10 +127,10 @@ bool FirebaseTest::RunFlakyBlockBase(bool (*flaky_block)(void* context),

firebase::FutureBase FirebaseTest::RunWithRetryBase(
firebase::FutureBase (*run_future)(void* context), void* context,
const char* name, int expected_error) {
const char* name, std::vector<int> expected_errors) {
// Run run_future(context), which returns a Future, then wait for that Future
// to complete. If the Future returns Invalid, or if its error() does
// not match expected_error, pause a moment and try again.
// to complete. If the Future returns Invalid, or if its error() is
// not present in expected_errors, pause a moment and try again.
//
// In most cases, this will return the Future once it's been completed.
// However, if it reaches the last attempt, it will return immediately once
Expand All @@ -140,6 +141,10 @@ firebase::FutureBase FirebaseTest::RunWithRetryBase(
const int kNumAttempts =
1 + (sizeof(kRetryDelaysMs) / sizeof(kRetryDelaysMs[0]));

if (expected_errors.empty()) {
expected_errors.push_back(0);
}

int attempt = 0;
firebase::FutureBase future;

Expand All @@ -158,10 +163,14 @@ firebase::FutureBase FirebaseTest::RunWithRetryBase(
app_framework::LogDebug(
"RunWithRetry%s%s: Attempt %d returned invalid status",
*name ? " " : "", name, attempt + 1);
} else if (future.error() != expected_error) {
} else if (std::find(expected_errors.begin(), expected_errors.end(),
future.error()) == std::end(expected_errors)) {
std::string expected_errors_str =
VariantToString(firebase::Variant(expected_errors));
app_framework::LogDebug(
"RunWithRetry%s%s: Attempt %d returned error %d, expected %d",
*name ? " " : "", name, attempt + 1, future.error(), expected_error);
"RunWithRetry%s%s: Attempt %d returned error %d, expected one of %s",
*name ? " " : "", name, attempt + 1, future.error(),
expected_errors_str.c_str());
} else {
// Future is completed and the error matches what's expected, no need to
// retry further.
Expand All @@ -178,18 +187,24 @@ firebase::FutureBase FirebaseTest::RunWithRetryBase(
}

bool FirebaseTest::WaitForCompletion(const firebase::FutureBase& future,
const char* name, int expected_error) {
const char* name,
std::vector<int> expected_errors) {
if (expected_errors.empty()) {
// If unspecified, default expected error is 0, success.
expected_errors.push_back(0);
}
app_framework::LogDebug("WaitForCompletion %s", name);
while (future.status() == firebase::kFutureStatusPending) {
app_framework::ProcessEvents(100);
}
EXPECT_EQ(future.status(), firebase::kFutureStatusComplete)
<< name << " returned an invalid status.";
EXPECT_EQ(future.error(), expected_error)
<< name << " returned error " << future.error() << ": "
EXPECT_THAT(expected_errors, testing::Contains(future.error()))
<< name << " returned unexpected error " << future.error() << ": "
<< future.error_message();
return (future.status() == firebase::kFutureStatusComplete &&
future.error() == expected_error);
std::find(expected_errors.begin(), expected_errors.end(),
future.error()) != std::end(expected_errors));
}

bool FirebaseTest::WaitForCompletionAnyResult(
Expand Down
47 changes: 33 additions & 14 deletions testing/test_framework/src/firebase_test_framework.h
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,20 @@ class FirebaseTest : public testing::Test {
// Google Play services version. Otherwise, returns 0.
static int GetGooglePlayServicesVersion();

// Returns true if the future completed with one of the expected
// error codes, fails the test and returns false otherwise.
static bool WaitForCompletion(const firebase::FutureBase& future,
const char* name,
std::vector<int> expected_errors = {});

// Returns true if the future completed as expected, fails the test and
// returns false otherwise.
static bool WaitForCompletion(const firebase::FutureBase& future,
const char* name, int expected_error = 0);
const char* name, int expected_error) {
std::vector<int> error_list;
error_list.push_back(expected_error);
return WaitForCompletion(future, name, error_list);
}

// Just wait for completion, not caring what the result is (as long as
// it's not Invalid). Returns true, unless Invalid.
Expand Down Expand Up @@ -429,7 +439,7 @@ class FirebaseTest : public testing::Test {
// exponential backoff if the operation fails.
//
// Blocks until the operation succeeds (the Future completes, with error
// matching expected_error) or if the final attempt is started (in which case
// matching expected_errors) or if the final attempt is started (in which case
// the Future returned may still be in progress). You should use
// WaitForCompletion to await the results of this function in any case.
//
Expand All @@ -445,10 +455,9 @@ class FirebaseTest : public testing::Test {
// return auth->DeleteUser(auth->current_user());
// }, auth_), "DeleteUser"));
template <class CallbackType, class ContextType>
static firebase::FutureBase RunWithRetry(CallbackType run_future_typed,
ContextType* context_typed,
const char* name = "",
int expected_error = 0) {
static firebase::FutureBase RunWithRetry(
CallbackType run_future_typed, ContextType* context_typed,
const char* name = "", std::vector<int> expected_errors = {}) {
struct RunData {
CallbackType callback;
ContextType* context;
Expand All @@ -460,7 +469,7 @@ class FirebaseTest : public testing::Test {
ContextType* context = static_cast<RunData*>(ctx)->context;
return static_cast<firebase::FutureBase>(callback(context));
},
static_cast<void*>(&run_data), name, expected_error);
static_cast<void*>(&run_data), name, expected_errors);
}

// Same as RunWithRetry, but templated to return a Future<ResultType>
Expand All @@ -470,7 +479,7 @@ class FirebaseTest : public testing::Test {
template <class ResultType, class CallbackType, class ContextType>
static firebase::Future<ResultType> RunWithRetry(
CallbackType run_future_typed, ContextType* context_typed,
const char* name = "", int expected_error = 0) {
const char* name = "", std::vector<int> expected_errors = {}) {
struct RunData {
CallbackType callback;
ContextType* context;
Expand All @@ -486,15 +495,15 @@ class FirebaseTest : public testing::Test {
firebase::Future<ResultType> future_result = callback(context);
return static_cast<firebase::FutureBase>(future_result);
},
static_cast<void*>(&run_data), name, expected_error);
static_cast<void*>(&run_data), name, expected_errors);
// Future<T> and FutureBase are reinterpret_cast-compatible, by design.
return *reinterpret_cast<firebase::Future<ResultType>*>(&result_base);
}

// Same as RunWithRetry above, but use std::function to allow captures.
static firebase::FutureBase RunWithRetry(
std::function<firebase::FutureBase()> run_future, const char* name = "",
int expected_error = 0) {
std::vector<int> expected_errors = {}) {
struct RunData {
std::function<firebase::FutureBase()>* callback;
};
Expand All @@ -504,13 +513,13 @@ class FirebaseTest : public testing::Test {
auto& callback = *static_cast<RunData*>(ctx)->callback;
return static_cast<firebase::FutureBase>(callback());
},
static_cast<void*>(&run_data), name, expected_error);
static_cast<void*>(&run_data), name, expected_errors);
}
// Same as RunWithRetry<type>, but use std::function to allow captures.
template <class ResultType>
static firebase::Future<ResultType> RunWithRetry(
std::function<firebase::Future<ResultType>()> run_future,
const char* name = "", int expected_error = 0) {
const char* name = "", std::vector<int> expected_errors = {}) {
struct RunData {
std::function<firebase::Future<ResultType>()>* callback;
};
Expand All @@ -524,7 +533,7 @@ class FirebaseTest : public testing::Test {
firebase::Future<ResultType> future_result = callback();
return static_cast<firebase::FutureBase>(future_result);
},
static_cast<void*>(&run_data), name, expected_error);
static_cast<void*>(&run_data), name, expected_errors);
// Future<T> and FutureBase are reinterpret_cast-compatible, by design.
return *reinterpret_cast<firebase::Future<ResultType>*>(&result_base);
}
Expand Down Expand Up @@ -569,7 +578,17 @@ class FirebaseTest : public testing::Test {
// for type safety.
static firebase::FutureBase RunWithRetryBase(
firebase::FutureBase (*run_future)(void* context), void* context,
const char* name, int expected_error);
const char* name, std::vector<int> expected_errors);

// Untyped version of RunWithRetry with one expected error.
static firebase::FutureBase RunWithRetryBase(
firebase::FutureBase (*run_future)(void* context), void* context,
const char* name, int expected_error) {
std::vector<int> error_list;
error_list.push_back(expected_error);
return RunWithRetryBase(run_future, context, name, error_list);
}

// Untyped version of RunFlakyBlock, with implementation.
// This is kept private because the templated version should be used instead,
// for type safety.
Expand Down
Loading