blob: 10b5fafe4d9c2ad3a836a7039ea377b62064e2ab [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/download/internal/background_service/scheduler/scheduler_impl.h"
#include <stdint.h>
#include <memory>
#include "base/strings/string_number_conversions.h"
#include "components/download/internal/background_service/config.h"
#include "components/download/internal/background_service/entry.h"
#include "components/download/internal/background_service/scheduler/device_status.h"
#include "components/download/public/background_service/task_scheduler.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::InSequence;
namespace download {
namespace {
class MockTaskScheduler : public TaskScheduler {
public:
MockTaskScheduler() = default;
~MockTaskScheduler() override = default;
MOCK_METHOD6(ScheduleTask,
void(DownloadTaskType, bool, bool, int, int64_t, int64_t));
MOCK_METHOD1(CancelTask, void(DownloadTaskType));
};
class DownloadSchedulerImplTest : public testing::Test {
public:
DownloadSchedulerImplTest() {}
~DownloadSchedulerImplTest() override = default;
void TearDown() override { DestroyScheduler(); }
void BuildScheduler(const std::vector<DownloadClient> clients) {
scheduler_ =
std::make_unique<SchedulerImpl>(&task_scheduler_, &config_, clients);
}
void DestroyScheduler() { scheduler_.reset(); }
// Helper function to create a list of entries for the scheduler to query the
// next entry.
void BuildDataEntries(size_t size) {
entries_ = std::vector<Entry>(size, Entry());
for (size_t i = 0; i < size; ++i) {
entries_[i].guid = base::IntToString(i);
entries_[i].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE;
entries_[i].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::UNMETERED;
entries_[i].state = Entry::State::AVAILABLE;
}
}
// Returns list of entry pointers to feed to the scheduler.
Model::EntryList entries() {
Model::EntryList entry_list;
for (auto& entry : entries_) {
entry_list.emplace_back(&entry);
}
return entry_list;
}
// Simulates the entry has been processed by the download service and the
// state has changed.
void MakeEntryActive(Entry* entry) {
if (entry)
entry->state = Entry::State::ACTIVE;
}
// Reverts the states of entry so that the scheduler can poll it again.
void MakeEntryAvailable(Entry* entry) {
entry->state = Entry::State::AVAILABLE;
}
// Helper function to build a device status.
DeviceStatus BuildDeviceStatus(BatteryStatus battery, NetworkStatus network) {
DeviceStatus device_status;
device_status.battery_status = battery;
device_status.network_status = network;
return device_status;
}
protected:
std::unique_ptr<SchedulerImpl> scheduler_;
MockTaskScheduler task_scheduler_;
Configuration config_;
// Entries owned by the test fixture.
std::vector<Entry> entries_;
private:
DISALLOW_COPY_AND_ASSIGN(DownloadSchedulerImplTest);
};
// Ensures normal polling logic is correct.
TEST_F(DownloadSchedulerImplTest, BasicPolling) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST_2,
DownloadClient::TEST});
// Client TEST: entry 0.
// Client TEST_2: entry 1.
// Poll sequence: 1 -> 0.
BuildDataEntries(2);
entries_[0].client = DownloadClient::TEST;
entries_[1].client = DownloadClient::TEST_2;
// First download belongs to first client.
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(next, &entries_[1]);
MakeEntryActive(next);
// If the first one is processed, the next should be the other entry.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(next, &entries_[0]);
MakeEntryActive(next);
}
// Tests the load balancing and polling downloads based on cancel time.
TEST_F(DownloadSchedulerImplTest, BasicLoadBalancing) {
BuildScheduler(std::vector<DownloadClient>{
DownloadClient::TEST, DownloadClient::TEST_2, DownloadClient::TEST_3});
// Client TEST: entry 0, entry 1 (earlier cancel time).
// Client TEST_2: entry 2.
// Client TEST_3: No entries.
// Poll sequence: 1 -> 2 -> 0.
BuildDataEntries(3);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(20);
entries_[1].client = DownloadClient::TEST;
entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(10);
entries_[2].client = DownloadClient::TEST_2;
entries_[2].scheduling_params.cancel_time = base::Time::FromInternalValue(30);
// There are 2 downloads for client 0, the one with earlier create time will
// be the next download.
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[1], next);
MakeEntryActive(next);
// The second download should belongs to client 1, because of the round robin
// load balancing.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[2], next);
MakeEntryActive(next);
// Only one entry left, which will be the next.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
MakeEntryActive(next);
// Keep polling twice, since no available downloads, both will return nullptr.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(nullptr, next);
MakeEntryActive(next);
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(nullptr, next);
MakeEntryActive(next);
}
// Ensures downloads are polled based on scheduling parameters and device
// status.
TEST_F(DownloadSchedulerImplTest, SchedulingParams) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST});
BuildDataEntries(1);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE;
entries_[0].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::UNMETERED;
Entry* next = nullptr;
// Tests network scheduling parameter.
// No downloads can be polled when network disconnected.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::DISCONNECTED));
EXPECT_EQ(nullptr, next);
// If the network is metered, and scheduling parameter requires unmetered
// network, the download should not be polled.
next = scheduler_->Next(entries(), BuildDeviceStatus(BatteryStatus::CHARGING,
NetworkStatus::METERED));
EXPECT_EQ(nullptr, next);
// If the network requirement is none, the download can happen under metered
// network. However, download won't happen when network is disconnected.
entries_[0].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::NONE;
next = scheduler_->Next(entries(), BuildDeviceStatus(BatteryStatus::CHARGING,
NetworkStatus::METERED));
EXPECT_EQ(&entries_[0], next);
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::DISCONNECTED));
EXPECT_EQ(nullptr, next);
MakeEntryActive(next);
// Tests battery sensitive scheduling parameter.
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE;
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
// Battery sensitive with low battery level.
DeviceStatus status =
BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED);
DCHECK_EQ(status.battery_percentage, 0);
next = scheduler_->Next(entries(), status);
EXPECT_EQ(nullptr, next);
status.battery_percentage = config_.download_battery_percentage - 1;
next = scheduler_->Next(entries(), status);
EXPECT_EQ(nullptr, next);
// Battery sensitive with high battery level.
status.battery_percentage = config_.download_battery_percentage;
next = scheduler_->Next(entries(), status);
EXPECT_EQ(&entries_[0], next);
// Tests battery insensitive scheduling parameter.
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE;
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
MakeEntryActive(next);
}
// Ensures higher priority will be scheduled first.
TEST_F(DownloadSchedulerImplTest, Priority) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST});
// The second entry has higher priority but is created later than the first
// entry. This ensures priority is checked before the create time.
BuildDataEntries(2);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(20);
entries_[1].client = DownloadClient::TEST;
entries_[1].scheduling_params.priority = SchedulingParams::Priority::HIGH;
entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(40);
// Download with higher priority should be polled first, even if there is
// another download created earlier.
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[1], next);
// Download with non UI priority should be subject to network and battery
// scheduling parameters. The higher priority one will be ignored because of
// mismatching battery condition.
entries_[1].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE;
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE;
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
MakeEntryActive(next);
}
// Ensures UI priority entries are subject to device status check.
TEST_F(DownloadSchedulerImplTest, UIPrioritySubjectToDeviceStatus) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST,
DownloadClient::TEST_2});
// Client TEST: entry 0.
// Client TEST_2: entry 1 (UI priority, cancel later).
BuildDataEntries(2);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[1].client = DownloadClient::TEST_2;
entries_[1].scheduling_params.priority = SchedulingParams::Priority::UI;
// UI priority is also subject to device status validation.
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::METERED));
EXPECT_EQ(nullptr, next);
MakeEntryActive(next);
}
// UI priority entries will be processed first even if they doesn't belong to
// the current client in load balancing.
TEST_F(DownloadSchedulerImplTest, UIPriorityLoadBalancing) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST,
DownloadClient::TEST_2});
// Client TEST: entry 0(Low priority).
// Client TEST_2: entry 1(UI priority).
BuildDataEntries(2);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[1].client = DownloadClient::TEST_2;
entries_[1].scheduling_params.priority = SchedulingParams::Priority::UI;
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[1], next);
MakeEntryActive(next);
}
TEST_F(DownloadSchedulerImplTest, PickOlderDownloadIfSameParameters) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST,
DownloadClient::TEST_2});
// Client TEST: entry 0(Low priority, No Cancel Time, Newer).
// Client TEST: entry 1(Low priority, No Cancel Time, Older).
// Client TEST: entry 2(Low priority, No Cancel Time, Newer).
BuildDataEntries(3);
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[0].create_time = base::Time::Now();
entries_[1].client = DownloadClient::TEST;
entries_[1].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[1].create_time = base::Time::Now() - base::TimeDelta::FromDays(1);
entries_[2].client = DownloadClient::TEST;
entries_[2].scheduling_params.priority = SchedulingParams::Priority::LOW;
entries_[2].create_time = base::Time::Now();
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[1], next);
MakeEntryActive(next);
}
// When multiple UI priority entries exist, the next entry is selected based on
// cancel time and load balancing.
TEST_F(DownloadSchedulerImplTest, MultipleUIPriorityEntries) {
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST,
DownloadClient::TEST_2});
BuildDataEntries(4);
// Client TEST: entry 0(UI priority), entry 1(UI priority, early cancel time).
// Client TEST_2: entry 2(UI priority), entry 3(high priority, early cancel
// time). Poll sequence: 1 -> 2 -> 0 -> 3.
for (auto& entry : entries_) {
entry.scheduling_params.priority = SchedulingParams::Priority::UI;
}
entries_[0].client = DownloadClient::TEST;
entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(40);
entries_[1].client = DownloadClient::TEST;
entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(20);
entries_[2].client = DownloadClient::TEST_2;
entries_[2].scheduling_params.cancel_time = base::Time::FromInternalValue(50);
entries_[3].client = DownloadClient::TEST_2;
entries_[3].scheduling_params.cancel_time = base::Time::FromInternalValue(20);
entries_[3].scheduling_params.priority = SchedulingParams::Priority::HIGH;
// When device conditions are meet, UI priority entry with the earliest cancel
// time will be processed first.
Entry* next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[1], next);
MakeEntryActive(next);
// Next entry will be UI priority entry from another client.
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[2], next);
MakeEntryActive(next);
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[0], next);
MakeEntryActive(next);
next = scheduler_->Next(
entries(),
BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED));
EXPECT_EQ(&entries_[3], next);
MakeEntryActive(next);
}
// Ensures the reschedule logic works correctly, and we can pass the correct
// criteria to platform task scheduler.
TEST_F(DownloadSchedulerImplTest, Reschedule) {
InSequence s;
BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST});
BuildDataEntries(2);
entries_[0].client = DownloadClient::TEST;
entries_[1].client = DownloadClient::TEST;
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_CHARGING;
entries_[0].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::UNMETERED;
entries_[1].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_CHARGING;
entries_[1].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::UNMETERED;
Criteria criteria(config_.download_battery_percentage);
EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK))
.Times(0);
EXPECT_CALL(task_scheduler_,
ScheduleTask(DownloadTaskType::DOWNLOAD_TASK,
criteria.requires_unmetered_network,
criteria.requires_battery_charging, _, _, _))
.RetiresOnSaturation();
scheduler_->Reschedule(entries());
entries_[0].scheduling_params.battery_requirements =
SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE;
criteria.requires_battery_charging = false;
EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK))
.Times(0);
EXPECT_CALL(task_scheduler_,
ScheduleTask(DownloadTaskType::DOWNLOAD_TASK,
criteria.requires_unmetered_network,
criteria.requires_battery_charging, _, _, _))
.RetiresOnSaturation();
scheduler_->Reschedule(entries());
entries_[0].scheduling_params.network_requirements =
SchedulingParams::NetworkRequirements::NONE;
criteria.requires_unmetered_network = false;
EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK))
.Times(0);
EXPECT_CALL(task_scheduler_,
ScheduleTask(DownloadTaskType::DOWNLOAD_TASK,
criteria.requires_unmetered_network,
criteria.requires_battery_charging, _, _, _))
.RetiresOnSaturation();
scheduler_->Reschedule(entries());
EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK))
.RetiresOnSaturation();
scheduler_->Reschedule(Model::EntryList());
}
} // namespace
} // namespace download