| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/app_mode/retry_runner.h" |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using base::test::TestFuture; |
| |
| namespace ash { |
| |
| class RetryRunnerTest : public testing::Test { |
| protected: |
| using ResultCallback = base::OnceCallback<void(std::optional<int> result)>; |
| |
| // Task environment is needed because we call `PostDelayedTask()`. |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| }; |
| |
| TEST_F(RetryRunnerTest, JobSucceedsOnFirstAttempt) { |
| auto job = [](ResultCallback on_result) { std::move(on_result).Run(42); }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/1, base::BindRepeating(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(42, result.Take()); |
| } |
| |
| TEST_F(RetryRunnerTest, JobSucceedsOnSecondAttempt) { |
| bool did_run = false; |
| auto job = [&](ResultCallback on_result) { |
| did_run ? std::move(on_result).Run(42) |
| : std::move(on_result).Run(std::nullopt); |
| did_run = true; |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/2, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(42, result.Take()); |
| } |
| |
| TEST_F(RetryRunnerTest, JobThatFailsReturnsNullopt) { |
| auto job = [](ResultCallback on_result) { |
| std::move(on_result).Run(std::nullopt); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/5, base::BindRepeating(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(std::nullopt, result.Take()); |
| } |
| |
| TEST_F(RetryRunnerTest, ReturnsOnFirstSuccessfulAttempt) { |
| int attempts = 0; |
| auto job = [&](ResultCallback on_result) { |
| attempts++; |
| attempts == 3 ? std::move(on_result).Run(42) |
| : std::move(on_result).Run(std::nullopt); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/5, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(42, result.Take()); |
| EXPECT_EQ(3, attempts); |
| } |
| |
| TEST_F(RetryRunnerTest, RetriesNTimes) { |
| int attempts = 0; |
| auto job = [&](ResultCallback on_result) { |
| attempts += 1; |
| std::move(on_result).Run(std::nullopt); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/5, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(std::nullopt, result.Take()); |
| EXPECT_EQ(5, attempts); |
| } |
| |
| TEST_F(RetryRunnerTest, DestroyingTaskCancelsIt) { |
| auto job = [](ResultCallback on_result) { |
| std::move(on_result).Run(std::nullopt); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/5, base::BindRepeating(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| handle.reset(); |
| |
| base::RunLoop loop; |
| loop.RunUntilIdle(); |
| EXPECT_FALSE(result.IsReady()); |
| } |
| |
| TEST_F(RetryRunnerTest, WorksWithCancellableJob) { |
| auto job = [&](ResultCallback on_result) { |
| std::move(on_result).Run(42); |
| return std::unique_ptr<CancellableJob>{}; |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/8, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(42, result.Take()); |
| } |
| |
| // Helper class that calls the given callback when destroyed. |
| class DestructorNotifer : public CancellableJob { |
| public: |
| explicit DestructorNotifer(base::OnceClosure on_destroy) |
| : on_destroy_(std::move(on_destroy)) {} |
| ~DestructorNotifer() override { std::move(on_destroy_).Run(); } |
| |
| private: |
| base::OnceClosure on_destroy_; |
| }; |
| |
| TEST_F(RetryRunnerTest, DestroysHandleAfterAttempts) { |
| constexpr int successful_attempt = 3; |
| TestFuture<void> all_destroyed; |
| auto on_destroy = base::BarrierClosure(3, all_destroyed.GetCallback()); |
| int attempts = 0; |
| auto job = [&](ResultCallback on_result) -> std::unique_ptr<CancellableJob> { |
| attempts++; |
| attempts == successful_attempt ? std::move(on_result).Run(42) |
| : std::move(on_result).Run(std::nullopt); |
| return std::make_unique<DestructorNotifer>(on_destroy); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/4, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| EXPECT_EQ(42, result.Take()); |
| EXPECT_EQ(successful_attempt, attempts); |
| EXPECT_TRUE(all_destroyed.IsReady()) << "Handle not destroyed"; |
| EXPECT_NE(handle, nullptr); |
| } |
| |
| TEST_F(RetryRunnerTest, KeepsHandleAliveForJobDuration) { |
| ResultCallback job_result; |
| TestFuture<void> destroyed; |
| auto job = [&](ResultCallback on_result) -> std::unique_ptr<CancellableJob> { |
| job_result = std::move(on_result); |
| return std::make_unique<DestructorNotifer>(destroyed.GetCallback()); |
| }; |
| |
| TestFuture<std::optional<int>> result; |
| auto handle = RunUpToNTimes<std::optional<int>>( |
| /*n=*/4, base::BindLambdaForTesting(job), RetryIfNullopt<int>(), |
| /*on_done=*/result.GetCallback()); |
| |
| // The job started and the handle has not been destroyed. |
| ASSERT_FALSE(job_result.is_null()) << "Job not started"; |
| EXPECT_FALSE(destroyed.IsReady()) << "Handle already destroyed"; |
| |
| // Then once the current job finishes, the handle is destroyed. |
| std::move(job_result).Run(std::nullopt); |
| EXPECT_TRUE(destroyed.Wait()) << "Handle not destroyed"; |
| |
| EXPECT_NE(handle, nullptr); |
| } |
| |
| } // namespace ash |