You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libcutefish/clients/upower/tests/upower_test.cpp

392 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtest/gtest.h>
#include <chrono>
#include <future>
#include <memory>
#include <thread>
#include <tuple>
#include "base/event_loop.h"
#include "cutefish/upower/battery_status.h"
#include "cutefish/upower/device.h"
using namespace cutefish;
using namespace cutefish::base;
using namespace cutefish::base::dbus;
using namespace cutefish::upower;
using namespace std::chrono_literals;
namespace {
constexpr const char* kDevicePath = "/org/freedesktop/UPower/devices/battery_BAT0";
constexpr const char* kDeviceInterface = "org.freedesktop.UPower.Device";
// cutefish-services' battery and UPower on the session bus, on a thread of
// their own as in other processes.
class FakeServices
{
public:
explicit FakeServices(bool battery)
: m_battery(battery)
{
std::promise<bool> ready;
m_thread = std::thread([this, &ready] { run(ready); });
m_ready = ready.get_future().get();
}
~FakeServices()
{
m_handle.post([this] { m_loop->quit(); });
m_thread.join();
}
bool isReady() const { return m_ready; }
// Runs on the services' thread.
template<typename F>
auto inService(F f)
{
std::promise<decltype(f())> result;
m_handle.post([&] {
if constexpr (std::is_void_v<decltype(f())>) {
f();
result.set_value();
} else {
result.set_value(f());
}
});
return result.get_future().get();
}
// cutefish-services' battery.
int32_t chargePercent = 64;
int64_t lastChargedSecs = 1700000000;
int refreshes = 0;
std::unique_ptr<Object> batteryObject;
// UPower.
bool onBattery = false;
bool thresholdEnabled = false;
bool enableAskedInteractively = false;
std::tuple<std::string, uint32_t, uint32_t> historyRequest;
std::unique_ptr<Object> upowerObject;
std::unique_ptr<Object> deviceObject;
double displayPercentage = 41.6;
std::unique_ptr<Object> displayObject;
private:
void run(std::promise<bool>& ready)
{
EventLoop loop;
m_loop = &loop;
m_handle = loop.handle();
std::unique_ptr<Connection> connection = Connection::open(Connection::Bus::Session, &loop);
if (!connection || !connection->requestName("com.cutefish.Services")
|| !connection->requestName("org.freedesktop.UPower")) {
ready.set_value(false);
return;
}
bool published = true;
if (m_battery) {
batteryObject = std::make_unique<Object>(connection.get(), "/com/cutefish/Services/Battery",
"com.cutefish.Services.Battery");
Object& battery = *batteryObject;
battery.addProperty("chargeState", [] { return int32_t(2); });
battery.addProperty("chargePercent", [this] { return chargePercent; });
battery.addProperty("capacity", [] { return int32_t(93); });
battery.addProperty("statusString", [] { return std::string("Discharging"); });
battery.addProperty("lastChargedPercent", [] { return int32_t(100); });
battery.addProperty("lastChargedSecs", [this] { return lastChargedSecs; });
battery.addProperty("udi", [] { return std::string(kDevicePath); });
battery.addMethod("refresh", [this] { ++refreshes; });
battery.addSignal("chargePercentChanged", "i");
published = battery.publish();
}
upowerObject = std::make_unique<Object>(connection.get(), "/org/freedesktop/UPower", "org.freedesktop.UPower");
upowerObject->addProperty("OnBattery", [this] { return onBattery; });
published = upowerObject->publish() && published;
deviceObject = std::make_unique<Object>(connection.get(), kDevicePath, kDeviceInterface);
Object& device = *deviceObject;
device.addProperty("NativePath", [] { return std::string("BAT0"); });
device.addProperty("ChargeThresholdSupported", [] { return true; });
device.addProperty("ChargeThresholdEnabled", [this] { return thresholdEnabled; });
device.addProperty("ChargeStartThreshold", [] { return uint32_t(70); });
device.addProperty("ChargeEndThreshold", [] { return uint32_t(90); });
device.addMethod("EnableChargeThreshold", "b", "", [this](MethodCall& call) {
bool enabled = false;
call.read(enabled);
enableAskedInteractively = call.message().allowsInteractiveAuthorization();
if (enabled == thresholdEnabled) {
call.replyError("org.freedesktop.UPower.GeneralError", "Unchanged");
return;
}
thresholdEnabled = enabled;
deviceObject->emitPropertiesChanged({"ChargeThresholdEnabled"});
call.reply();
});
device.addMethod("GetHistory", [this](const std::string& type, uint32_t timespan, uint32_t resolution) {
historyRequest = {type, timespan, resolution};
// Newest first, as UPower returns them.
return std::vector<std::tuple<uint32_t, double, uint32_t>>{{300, 80.5, 1}, {200, 70, 2}, {100, 75, 2}};
});
published = device.publish() && published;
displayObject = std::make_unique<Object>(connection.get(), "/org/freedesktop/UPower/devices/DisplayDevice",
kDeviceInterface);
displayObject->addProperty("IsPresent", [this] { return m_battery; });
displayObject->addProperty("Type", [this] { return uint32_t(m_battery ? 2 : 0); });
displayObject->addProperty("Percentage", [this] { return displayPercentage; });
published = displayObject->publish() && published;
ready.set_value(published);
loop.run();
batteryObject.reset();
upowerObject.reset();
deviceObject.reset();
displayObject.reset();
}
bool m_battery;
std::thread m_thread;
EventLoop* m_loop = nullptr;
LoopHandle m_handle;
bool m_ready = false;
};
class UPowerTest : public testing::Test
{
protected:
void SetUp() override
{
if (!Connection::session(&m_loop))
GTEST_SKIP() << "No session bus; run the tests in dbus-run-session";
}
void startServices(bool battery = true)
{
m_services = std::make_unique<FakeServices>(battery);
ASSERT_TRUE(m_services->isReady());
}
// Runs the loop until the condition holds or two seconds pass.
bool waitFor(const std::function<bool()>& condition)
{
const auto deadline = std::chrono::steady_clock::now() + 2s;
while (!condition()) {
if (std::chrono::steady_clock::now() > deadline)
return false;
runFor(5ms);
}
return true;
}
void runFor(std::chrono::milliseconds time)
{
const int timer = m_loop.startTimer(time, false, [this] { m_loop.quit(); });
m_loop.run();
m_loop.stopTimer(timer);
}
Connection* bus() { return Connection::session(&m_loop); }
EventLoop m_loop;
std::unique_ptr<FakeServices> m_services;
};
} // namespace
TEST_F(UPowerTest, ReadyWithoutABattery)
{
startServices(false);
BatteryStatus status(bus(), bus());
ASSERT_TRUE(waitFor([&] { return status.isReady(); }));
EXPECT_FALSE(status.isAvailable());
EXPECT_EQ(status.chargePercent(), 0);
}
TEST_F(UPowerTest, BatteryStatusReadsAndFollowsTheService)
{
startServices();
BatteryStatus status(bus(), bus());
int available = 0;
Subscription availableSubscription = status.availableChanged.connect([&] { ++available; });
ASSERT_TRUE(waitFor([&] { return status.isReady(); }));
EXPECT_TRUE(status.isAvailable());
EXPECT_EQ(available, 1);
EXPECT_EQ(status.chargeState(), 2);
EXPECT_EQ(status.chargePercent(), 64);
EXPECT_EQ(status.capacity(), 93);
EXPECT_EQ(status.statusString(), "Discharging");
EXPECT_EQ(status.lastChargedPercent(), 100);
EXPECT_EQ(status.lastChargedSecs(), 1700000000);
EXPECT_EQ(status.udi(), kDevicePath);
// A change signal is only a cue: the values come from a fresh read.
int percent = 0;
Subscription percentSubscription = status.chargePercentChanged.connect([&] { ++percent; });
m_services->inService([&] {
m_services->chargePercent = 65;
m_services->batteryObject->emitSignal("chargePercentChanged", int32_t(0));
});
ASSERT_TRUE(waitFor([&] { return status.chargePercent() == 65; }));
EXPECT_EQ(percent, 1);
status.refresh();
ASSERT_TRUE(waitFor([&] { return m_services->inService([&] { return m_services->refreshes; }) == 1; }));
}
TEST_F(UPowerTest, ThePowerSourceFollowsUPower)
{
startServices();
BatteryStatus status(bus(), bus());
ASSERT_TRUE(waitFor([&] { return status.isReady(); }));
EXPECT_FALSE(status.onBattery());
int changes = 0;
Subscription subscription = status.onBatteryChanged.connect([&] { ++changes; });
m_services->inService([&] {
m_services->onBattery = true;
m_services->upowerObject->emitPropertiesChanged({"OnBattery"});
});
ASSERT_TRUE(waitFor([&] { return status.onBattery(); }));
EXPECT_EQ(changes, 1);
// The service's figures depend on the power source, so it reads again.
ASSERT_TRUE(waitFor([&] { return m_services->inService([&] { return m_services->refreshes; }) == 1; }));
}
TEST_F(UPowerTest, WithoutASessionBusTheDisplayDeviceIsRead)
{
startServices();
BatteryStatus status(nullptr, bus());
ASSERT_TRUE(waitFor([&] { return status.isReady(); }));
EXPECT_TRUE(status.isAvailable());
EXPECT_EQ(status.chargePercent(), 42);
EXPECT_EQ(status.statusString(), "");
int percent = 0;
Subscription subscription = status.chargePercentChanged.connect([&] { ++percent; });
m_services->inService([&] {
m_services->displayPercentage = 80.2;
m_services->displayObject->emitPropertiesChanged({"Percentage"});
});
ASSERT_TRUE(waitFor([&] { return status.chargePercent() == 80; }));
EXPECT_EQ(percent, 1);
// Nothing asks a service that is not there.
status.refresh();
runFor(50ms);
EXPECT_EQ(m_services->inService([&] { return m_services->refreshes; }), 0);
}
TEST_F(UPowerTest, WithoutASessionBusOrABatteryNothingIsAvailable)
{
startServices(false);
BatteryStatus status(nullptr, bus());
ASSERT_TRUE(waitFor([&] { return status.isReady(); }));
EXPECT_FALSE(status.isAvailable());
}
TEST_F(UPowerTest, SharedStatusIsOneWhileHeld)
{
std::shared_ptr<BatteryStatus> first = BatteryStatus::shared();
std::shared_ptr<BatteryStatus> second = BatteryStatus::shared();
EXPECT_EQ(first, second);
std::weak_ptr<BatteryStatus> held = first;
first.reset();
second.reset();
EXPECT_TRUE(held.expired());
EXPECT_TRUE(BatteryStatus::shared());
}
TEST_F(UPowerTest, DeviceReadsItsChargeLimit)
{
startServices();
Device device(kDevicePath, bus());
int changes = 0;
Subscription subscription = device.propertiesChanged.connect([&](const VariantMap&) { ++changes; });
ASSERT_TRUE(waitFor([&] { return device.isReady(); }));
EXPECT_EQ(changes, 1);
EXPECT_EQ(device.nativePath(), "BAT0");
EXPECT_TRUE(device.chargeThresholdSupported());
EXPECT_FALSE(device.chargeThresholdEnabled());
EXPECT_EQ(device.chargeStartThreshold(), 70);
EXPECT_EQ(device.chargeEndThreshold(), 90);
}
TEST_F(UPowerTest, EnablingTheChargeLimitMayAskAndAnnouncesIt)
{
startServices();
Device device(kDevicePath, bus());
ASSERT_TRUE(waitFor([&] { return device.isReady(); }));
bool answered = false;
Error error;
device.enableChargeThreshold(true, [&](const Error& e) {
answered = true;
error = e;
});
ASSERT_TRUE(waitFor([&] { return answered; }));
EXPECT_FALSE(error.isSet());
EXPECT_TRUE(m_services->inService([&] { return m_services->enableAskedInteractively; }));
ASSERT_TRUE(waitFor([&] { return device.chargeThresholdEnabled(); }));
// A refusal comes back as an error.
answered = false;
device.enableChargeThreshold(true, [&](const Error& e) {
answered = true;
error = e;
});
ASSERT_TRUE(waitFor([&] { return answered; }));
EXPECT_TRUE(error.isSet());
}
TEST_F(UPowerTest, HistoryComesBackAsSamples)
{
startServices();
Device device(kDevicePath, bus());
std::optional<std::vector<HistorySample>> samples;
bool answered = false;
PendingCall call = device.history(Device::HistoryType::Charge, 86400, 100,
[&](const std::optional<std::vector<HistorySample>>& result) {
samples = result;
answered = true;
});
ASSERT_TRUE(waitFor([&] { return answered; }));
ASSERT_TRUE(samples);
ASSERT_EQ(samples->size(), 3u);
EXPECT_EQ((*samples)[0].time, 300u);
EXPECT_EQ((*samples)[0].value, 80.5);
EXPECT_EQ((*samples)[0].state, 1u);
EXPECT_EQ((*samples)[2].time, 100u);
const auto request = m_services->inService([&] { return m_services->historyRequest; });
EXPECT_EQ(std::get<0>(request), "charge");
EXPECT_EQ(std::get<1>(request), 86400u);
EXPECT_EQ(std::get<2>(request), 100u);
answered = false;
call = device.history(Device::HistoryType::Rate, 600, 10,
[&](const std::optional<std::vector<HistorySample>>& result) {
samples = result;
answered = true;
});
ASSERT_TRUE(waitFor([&] { return answered; }));
EXPECT_EQ(std::get<0>(m_services->inService([&] { return m_services->historyRequest; })), "rate");
}
TEST_F(UPowerTest, HistoryOfAMissingDeviceIsEmpty)
{
startServices();
Device device("/org/freedesktop/UPower/devices/battery_BAT9", bus());
bool answered = false;
std::optional<std::vector<HistorySample>> samples = std::vector<HistorySample>{};
PendingCall call = device.history(Device::HistoryType::Charge, 86400, 100,
[&](const std::optional<std::vector<HistorySample>>& result) {
samples = result;
answered = true;
});
ASSERT_TRUE(waitFor([&] { return answered; }));
EXPECT_FALSE(samples);
}