Commit eada1e17 authored by Harald Sitter's avatar Harald Sitter 🏳️‍🌈
Browse files

add a simulation mode based on the fixture pool

it's very tricky to actively test the UI because you can't just inject
arbitrary SMART data blobs.

this refactor seeks to remedy that by abstracting away the device
notification from solid and based on that implement a complete
simulation mode where both the device notifier (the bit enumerating
solid) and the ctl (the bit doing kauth + smartctl) are replaced by
mocking instances at runtime. these instances then use the data in the
fixture pool instead of actually looking at the active hardware on the
system. this effectively allows us to check how all the various fixtures
are actually handled by the actual production code.

for convenience more than anything this is on by default when building
in debug mode. also for convenience the fixtures are rcc'd into the
binary instead of looked up from disk (also relocatability is of course
always a concern)

(this actually reveals a problem with the notification tech: it doesn't
notify on devices that are failing right out the gate since a fix that
changed how devices are added to the monitor :()
parent 433e536b
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
cmake_minimum_required(VERSION 3.16)
......@@ -15,6 +15,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${ECM_MODULE_PATH})
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CMakeDependentOption)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
......@@ -41,6 +42,13 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
find_package(smartctl)
set_package_properties(smartctl PROPERTIES TYPE RUNTIME)
cmake_dependent_option(WITH_SIMULATION
"Build with simulation tech allowing easy testing via PLASMA_DISKS_SIMULATION=1"
ON
"CMAKE_BUILD_TYPE MATCHES [Dd]ebug"
OFF
)
add_subdirectory(src)
if(BUILD_TESTING)
......
<!DOCTYPE RCC>
<!--
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: none
-->
<RCC version='1.0'>
<qresource prefix='/plasma-disks/fixtures/'>
<file alias='pass-freebsd.json'>./pass-freebsd.json</file>
<file alias='broken.json'>./broken.json</file>
<file alias='error-info-log-failed.json'>./error-info-log-failed.json</file>
<file alias='failing-sectors-passing-status.json'>./failing-sectors-passing-status.json</file>
<file alias='fail.json'>./fail.json</file>
<file alias='missing-status.json'>./missing-status.json</file>
<file alias='pass.json'>./pass.json</file>
</qresource>
</RCC>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
// SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QObject>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QTest>
#include <functional>
#include "devicenotifier.h"
#include <device.h>
#include <smartctl.h>
#include <smartmonitor.h>
class MockCtl : public AbstractSMARTCtl
{
public:
void run(const QString &devicePath) override
{
qDebug() << devicePath;
emit finished(devicePath, m_docs.value(devicePath));
}
QMap<QString, QJsonDocument> m_docs;
};
class SMARTMonitorTest : public QObject
{
Q_OBJECT
struct Payload {
QJsonDocument doc;
bool err = true;
};
private Q_SLOTS:
void load(const QString &fixture, Payload &payload)
{
payload.doc = QJsonDocument();
payload.err = true;
QFile file(QFINDTESTDATA(fixture));
QVERIFY(file.open(QFile::ReadOnly));
payload.doc = QJsonDocument::fromJson(file.readAll());
payload.err = false;
}
void testRun()
{
// Mock smartctl. We want fixed behavior.
auto ctl = new MockCtl; // new; monitor takes ownership!
Payload payload;
load("fixtures/pass.json", payload);
if (payload.err) {
return;
}
ctl->m_docs["/dev/testfoobarpass"] = payload.doc;
load("fixtures/fail.json", payload);
if (payload.err) {
return;
}
ctl->m_docs["/dev/testfoobarfail"] = payload.doc;
// NOTE: monitor still talks to solid but we aren't interested in its results
// to also inject our fixtures we manually product device discoveries here.
SMARTMonitor monitor(ctl);
// don't start it, that'd only run solid stuff that we do not test here
struct Ctl : public AbstractSMARTCtl {
void run(const QString &devicePath) override
{
static QMap<QString, QString> data{{"/dev/testfoobarpass", "fixtures/pass.json"}, {"/dev/testfoobarfail", "fixtures/fail.json"}};
const QString fixture = data.value(devicePath);
Q_ASSERT(!fixture.isEmpty());
QFile file(QFINDTESTDATA(fixture));
const bool open = file.open(QFile::ReadOnly);
Q_ASSERT(open);
QJsonParseError err;
const auto document = QJsonDocument::fromJson(file.readAll(), &err);
Q_ASSERT(err.error == QJsonParseError::NoError);
Q_EMIT finished(devicePath, document);
}
};
monitor.checkDevice(new Device{"udi-pass", "product", "/dev/testfoobarpass"});
// discover this twice to ensure notifications aren't duplicated!
monitor.checkDevice(new Device{"udi-fail", "product", "/dev/testfoobarfail"});
monitor.checkDevice(new Device{"udi-fail", "product", "/dev/testfoobarfail"});
struct Notifier : public DeviceNotifier {
using DeviceNotifier::DeviceNotifier;
void start() override
{
loadData();
}
void loadData() override
{
Q_EMIT addDevice(new Device{"udi-pass", "product", "/dev/testfoobarpass"});
// discover this twice to ensure notifications aren't duplicated!
Q_EMIT addDevice(new Device{"udi-fail", "product", "/dev/testfoobarfail"});
Q_EMIT addDevice(new Device{"udi-fail", "product", "/dev/testfoobarfail"});
}
};
SMARTMonitor monitor(std::make_unique<Ctl>(), std::make_unique<Notifier>());
QSignalSpy spy(&monitor, &SMARTMonitor::deviceAdded);
QVERIFY(spy.isValid());
monitor.start();
// The signals are all emitted in one go and as such should arrive
// within a single wait.
QVERIFY(spy.wait());
QCOMPARE(spy.count(), 2); // There are 3 devices but one is a dupe.
QCOMPARE(monitor.devices().count(), 2); // There are 3 devices but one is a dupe.
bool sawPass = false;
bool sawFail = false;
......
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
add_definitions(-DTRANSLATION_DOMAIN=\"plasma_disks\")
......@@ -10,8 +10,18 @@ set(kded_SRCS
smartnotifier.cpp
dbusobjectmanagerserver.cpp
device.cpp
devicenotifier.cpp
soliddevicenotifier.cpp
)
if(WITH_SIMULATION)
list(APPEND kded_SRCS
simulationdevicenotifier.cpp
simulationctl.cpp
../autotests/fixtures/simulation.qrc)
add_definitions(-DWITH_SIMULATION)
endif()
ecm_qt_declare_logging_category(
kded_SRCS
HEADER "kded_debug.h"
......
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#include "devicenotifier.h"
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#pragma once
#include <QObject>
class Device;
/** Abstraction interface for device discovery. Implemented as a Solid variant for example. */
class DeviceNotifier : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
virtual void start() = 0;
virtual void loadData() = 0;
Q_SIGNALS:
void addDevice(Device *device);
void removeUDI(const QString &udi);
};
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
// SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
#include <KDEDModule>
#include <KPluginFactory>
......@@ -7,8 +7,42 @@
#include "dbusobjectmanagerserver.h"
#include "device.h"
#include "smartctl.h"
#include "smartmonitor.h"
#include "smartnotifier.h"
#include "soliddevicenotifier.h"
#ifdef WITH_SIMULATION
#include "simulationctl.h"
#include "simulationdevicenotifier.h"
#endif
static bool isSimulation()
{
return qEnvironmentVariableIntValue("PLASMA_DISKS_SIMULATION") == 1;
}
template<class... Args>
static std::unique_ptr<AbstractSMARTCtl> make_unique_smartctl(Args &&... args)
{
#ifdef WITH_SIMULATION
if (isSimulation()) {
return std::make_unique<SimulationCtl>(std::forward<Args>(args)...);
}
#endif
return std::make_unique<SMARTCtl>(std::forward<Args>(args)...);
}
template<class... Args>
static std::unique_ptr<DeviceNotifier> make_unique_devicenotifier(Args &&... args)
{
#ifdef WITH_SIMULATION
if (isSimulation()) {
return std::make_unique<SimulationDeviceNotifier>(std::forward<Args>(args)...);
}
#endif
return std::make_unique<SolidDeviceNotifier>(std::forward<Args>(args)...);
}
class SMARTModule : public KDEDModule
{
......@@ -17,6 +51,7 @@ public:
explicit SMARTModule(QObject *parent, const QVariantList &args)
: KDEDModule(parent)
{
Q_INIT_RESOURCE(simulation);
Q_UNUSED(args);
connect(&m_monitor, &SMARTMonitor::deviceAdded, this, [this](Device *device) {
dbusDeviceServer.serve(device);
......@@ -28,7 +63,7 @@ public:
}
private:
SMARTMonitor m_monitor{new SMARTCtl};
SMARTMonitor m_monitor{make_unique_smartctl(), make_unique_devicenotifier()};
SMARTNotifier m_notifier{&m_monitor};
KDBusObjectManagerServer dbusDeviceServer;
};
......
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#include "simulationctl.h"
#include <QFile>
void SimulationCtl::run(const QString &devicePath)
{
qDebug() << "SIMULATING" << devicePath;
QFile file(devicePath);
Q_ASSERT(file.open(QIODevice::ReadOnly | QIODevice::Text));
QJsonParseError error;
const auto document = QJsonDocument::fromJson(file.readAll(), &error);
Q_ASSERT(error.error == QJsonParseError::NoError);
Q_EMIT finished(devicePath, document);
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#pragma once
#include "smartctl.h"
class SimulationCtl : public AbstractSMARTCtl
{
Q_OBJECT
public:
void run(const QString &devicePath) override;
};
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#include "simulationdevicenotifier.h"
#include <QDir>
#include <QDirIterator>
#include "device.h"
void SimulationDeviceNotifier::start()
{
loadData();
}
#include <QDebug>
void SimulationDeviceNotifier::loadData()
{
QDirIterator it(QStringLiteral(":/plasma-disks/fixtures/"), {QStringLiteral("*.json")});
while (it.hasNext()) {
it.next();
const auto info = it.fileInfo();
Q_EMIT addDevice(new Device(info.fileName(), info.fileName(), info.absoluteFilePath()));
}
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
#pragma once
#include "devicenotifier.h"
class SimulationDeviceNotifier : public DeviceNotifier
{
Q_OBJECT
public:
using DeviceNotifier::DeviceNotifier;
void start() override;
void loadData() override;
};
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
// SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
#include "smartmonitor.h"
#include <Solid/Block>
#include <Solid/Device>
#include <Solid/DeviceInterface>
#include <Solid/DeviceNotifier>
#include <Solid/StorageDrive>
#include <Solid/StorageVolume>
#include <QDebug>
#include <KLocalizedString>
#include "device.h"
#include "devicenotifier.h"
#include "kded_debug.h"
#include "smartctl.h"
#include "smartdata.h"
SMARTMonitor::SMARTMonitor(AbstractSMARTCtl *ctl, QObject *parent)
SMARTMonitor::SMARTMonitor(std::unique_ptr<AbstractSMARTCtl> ctl, std::unique_ptr<DeviceNotifier> deviceNotifier, QObject *parent)
: QObject(parent)
, m_ctl(ctl)
, m_ctl(std::move(ctl))
, m_deviceNotifier(std::move(deviceNotifier))
{
connect(&m_reloadTimer, &QTimer::timeout, this, &SMARTMonitor::reloadData);
connect(ctl, &AbstractSMARTCtl::finished, this, &SMARTMonitor::onSMARTCtlFinished);
connect(m_ctl.get(), &AbstractSMARTCtl::finished, this, &SMARTMonitor::onSMARTCtlFinished);
m_reloadTimer.setInterval(1000 * 60 /*minute*/ * 60 /*hour*/ * 24 /*day*/);
}
void SMARTMonitor::start()
{
qCDebug(KDED) << "starting";
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &SMARTMonitor::checkUDI);
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &SMARTMonitor::removeUDI);
QMetaObject::invokeMethod(this, &SMARTMonitor::reloadData);
connect(m_deviceNotifier.get(), &DeviceNotifier::addDevice, this, &SMARTMonitor::addDevice);
connect(m_deviceNotifier.get(), &DeviceNotifier::removeUDI, this, &SMARTMonitor::removeUDI);
QMetaObject::invokeMethod(m_deviceNotifier.get(), &DeviceNotifier::start, Qt::QueuedConnection); // async to ensure listeners are ready
}
QVector<Device *> SMARTMonitor::devices() const
......@@ -39,12 +34,6 @@ QVector<Device *> SMARTMonitor::devices() const
return m_devices;
}
void SMARTMonitor::checkUDI(const QString &udi)
{
Solid::Device dev(udi);
checkDevice(dev);
}
void SMARTMonitor::removeUDI(const QString &udi)
{
auto newEnd = std::remove_if(m_devices.begin(), m_devices.end(), [this, udi](Device *dev) {
......@@ -61,10 +50,7 @@ void SMARTMonitor::removeUDI(const QString &udi)
void SMARTMonitor::reloadData()
{
const auto devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume);
for (const auto &device : devices) {
checkDevice(device);
}
m_deviceNotifier->loadData();
m_reloadTimer.start();
}
......@@ -84,7 +70,9 @@ void SMARTMonitor::onSMARTCtlFinished(const QString &devicePath, const QJsonDocu
}
SMARTData data(document);
Q_ASSERT(devicePath == data.m_device);
if (!devicePath.endsWith(QStringLiteral(".json"))) { // simulation data
Q_ASSERT(devicePath == data.m_device);
}
auto existingIt = std::find_if(m_devices.begin(), m_devices.end(), [&device](Device *existing) {
return *existing == *device;
......@@ -104,45 +92,10 @@ void SMARTMonitor::onSMARTCtlFinished(const QString &devicePath, const QJsonDocu
emit deviceAdded(device);
}
void SMARTMonitor::checkDevice(const Solid::Device &device)
{
qCDebug(KDED) << "!!!! " << device.udi();
// This seems fairly awkward on a solid level. The actual device
// isn't really trivial to identify. It certainly mustn't be a
// filesystem but beyond that it's entirely unclear.
// The trouble here is that we'll only want to run smartctl on
// actual devices, not the partitions on the devices as otherwise
// we'll have trouble validating the output as we'd not know
// if it is incomplete because the device wasn't a device or
// there's no data or smartctl is broken or the auth helper is broken...
if (!device.is<Solid::StorageVolume>()) {
qCDebug(KDED) << " not a volume";
return; // certainly not an interesting device
}
switch (device.as<Solid::StorageVolume>()->usage()) {
case Solid::StorageVolume::Unused:
Q_FALLTHROUGH();
case Solid::StorageVolume::FileSystem:
Q_FALLTHROUGH();
case Solid::StorageVolume::Encrypted:
Q_FALLTHROUGH();
case Solid::StorageVolume::Other:
Q_FALLTHROUGH();
case Solid::StorageVolume::Raid:
qCDebug(KDED) << " bad type" << device.as<Solid::StorageVolume>()->usage();
return;
case Solid::StorageVolume::PartitionTable:
break;
}
qCDebug(KDED) << "evaluating!";
checkDevice(new Device(device));
}
void SMARTMonitor::checkDevice(Device *device)
void SMARTMonitor::addDevice(Device *device)
{
m_pendingDevices[device->path()] = device;
m_ctl->run(device->path());
}
#include "smartmonitor.moc"
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
// SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
#ifndef SMARTMONITOR_H
#define SMARTMONITOR_H
......@@ -11,13 +11,9 @@
#include <functional>
#include <memory>
#include "smartctl.h"
class Device;
namespace Solid
{
class AbstractSMARTCtl;
class Device;
}
class DeviceNotifier;
class SMARTMonitor : public QObject
{
......@@ -25,7 +21,7 @@ class SMARTMonitor : public QObject
friend class SMARTMonitorTest;
public:
explicit SMARTMonitor(AbstractSMARTCtl *ctl, QObject *parent = nullptr);
explicit SMARTMonitor(std::unique_ptr<AbstractSMARTCtl> ctl, std::unique_ptr<DeviceNotifier> deviceNotifier, QObject *parent = nullptr);
void start();
QVector<Device *> devices() const;
......@@ -35,17 +31,16 @@ signals:
void deviceRemoved(Device *device);
private slots:
void checkUDI(const QString &udi);
void removeUDI(const QString &udi);
void reloadData();
void onSMARTCtlFinished(const QString &devicePath, const QJsonDocument &document);
private:
void checkDevice(const Solid::Device &device);
void checkDevice(Device *device);
void addDevice(Device *device);
QTimer m_reloadTimer;
std::unique_ptr<AbstractSMARTCtl> m_ctl;
const std::unique_ptr<AbstractSMARTCtl> m_ctl;
const std::unique_ptr<DeviceNotifier> m_deviceNotifier;
QHash<QString, Device *> m_pendingDevices; // waiting for smartctl to return
QVector<Device *> m_devices; // monitored smart devices
};
......
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
#include "soliddevicenotifier.h"
#include <Solid/Block>
#include <Solid/DeviceInterface>
#include <Solid/DeviceNotifier>
#include <Solid/StorageDrive>
#include <Solid/StorageVolume>
#include "device.h"
#include "kded_debug.h"
void SolidDeviceNotifier::start()
{
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &SolidDeviceNotifier::checkUDI);
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &SolidDeviceNotifier::removeUDI);
loadData();
}
void SolidDeviceNotifier::loadData()
{
const auto devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume);
for (const auto &device : devices) {
checkSolidDevice(device);
}
}
void SolidDeviceNotifier::checkUDI(const QString &udi)
{
checkSolidDevice(Solid::Device(udi));
}
void SolidDeviceNotifier::checkSolidDevice(const Solid::Device &device)
{
qCDebug(KDED) << "!!!! " << device.udi();
// This seems fairly awkward on a solid level. The actual device
// isn't really trivial to identify. It certainly mustn't be a
// filesystem but beyond that it's entirely unclear.
// The trouble here is that we'll only want to run smartctl on
// actual devices, not the partitions on the devices as otherwise
// we'll have trouble validating the output as we'd not know
// if it is incomplete because the device wasn't a device or
// there's no data or smartctl is broken or the auth helper is broken...
if (!device.is<Solid::StorageVolume>()) {
qCDebug(KDED) << " not a volume";
return; // certainly not an interesting device
}
switch (device.as<Solid::StorageVolume>()->usage()) {
case Solid::StorageVolume::Unused:
Q_FALLTHROUGH();
case Solid::StorageVolume::FileSystem:
Q_FALLTHROUGH();
case Solid::StorageVolume::Encrypted:
Q_FALLTHROUGH();
case Solid::StorageVolume::Other:
Q_FALLTHROUGH();
case Solid::StorageVolume::Raid:
qCDebug(KDED) << " bad type" << device.as<Solid::StorageVolume>()->usage();
return;
case Solid::StorageVolume::PartitionTable:
break;
}
qCDebug