feat(SQ/Aggregator): Created utility `Aggregator`
- Abstract class created: `Aggregator` - Derived abstract class created: `SingleRoleAggregator` - Derived class created: `SumAggregator` - `Storybook` page created. - Added unit tests. Closes #12685
This commit is contained in:
parent
e7c4cd7c6c
commit
c2bfc6b8f4
|
@ -0,0 +1,174 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property string balanceRoleName: "balance"
|
||||
property string roleName: balanceRoleName
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: srcModel
|
||||
|
||||
ListElement {
|
||||
key: "ETH"
|
||||
|
||||
balances: [
|
||||
ListElement { chainId: "1"; balance: 3 },
|
||||
ListElement { chainId: "2"; balance: 4 },
|
||||
ListElement { chainId: "31"; balance: 2 }
|
||||
]
|
||||
}
|
||||
|
||||
ListElement {
|
||||
key: "SNT"
|
||||
|
||||
balances: [
|
||||
ListElement { chainId: "2"; balance: 42 }
|
||||
]
|
||||
}
|
||||
|
||||
ListElement {
|
||||
key: "DAI"
|
||||
|
||||
balances: [
|
||||
ListElement { chainId: "1"; balance: 4 },
|
||||
ListElement { chainId: "3"; balance: 9 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 40
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
border.color: "gray"
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
model: srcModel
|
||||
spacing: 10
|
||||
|
||||
header: Label {
|
||||
height: implicitHeight * 2
|
||||
text: `Source model (${srcModel.count})`
|
||||
|
||||
font.bold: true
|
||||
color: "blue"
|
||||
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
height: implicitHeight
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 4
|
||||
|
||||
text: `KEY: ${model.key}`
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 4
|
||||
|
||||
text: `Total balances: ${aggregator.value}`
|
||||
font.bold: true
|
||||
color: "red"
|
||||
}
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
model: balances
|
||||
delegate: Label {
|
||||
height: implicitHeight
|
||||
width: ListView.view.width
|
||||
text: `chainID: ${model.chainId}, balance: ${model.balance}`
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
SumAggregator {
|
||||
id: aggregator
|
||||
|
||||
model: balances
|
||||
roleName: d.roleName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Button {
|
||||
id: addRows
|
||||
text: "Add rows"
|
||||
onClicked: {
|
||||
srcModel.get(0).balances.append( {"chainId": "1", "balance": Math.random()} )
|
||||
srcModel.get(1).balances.append( {"chainId": "22", "balance": Math.random()} )
|
||||
srcModel.get(2).balances.append( {"chainId": "34", "balance": Math.random()} )
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: removeRows
|
||||
text: "Remove rows"
|
||||
onClicked: {
|
||||
if(srcModel.get(0).balances.count > 1)
|
||||
srcModel.get(0).balances.remove(0)
|
||||
if(srcModel.get(1).balances.count > 1)
|
||||
srcModel.get(1).balances.remove(0)
|
||||
if(srcModel.get(2).balances.count > 1)
|
||||
srcModel.get(2).balances.remove(0)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: resetModel
|
||||
text: "Reset model"
|
||||
onClicked: {
|
||||
srcModel.get(0).balances.clear()
|
||||
srcModel.get(1).balances.clear()
|
||||
srcModel.get(2).balances.clear()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: changeData
|
||||
text: "Change data"
|
||||
onClicked: {
|
||||
srcModel.get(0).balances.get(0).balance = Math.random()
|
||||
srcModel.get(1).balances.get(0).balance = Math.random()
|
||||
srcModel.get(2).balances.get(0).balance = Math.random()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: changeRoleName
|
||||
text: "Change role name"
|
||||
|
||||
onClicked: {
|
||||
if(d.roleName === d.balanceRoleName)
|
||||
d.roleName = "chainId"
|
||||
else
|
||||
d.roleName = d.balanceRoleName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Models
|
|
@ -98,6 +98,9 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/statuswindow.h
|
||||
include/StatusQ/stringutilsinternal.h
|
||||
include/StatusQ/submodelproxymodel.h
|
||||
include/StatusQ/aggregator.h
|
||||
include/StatusQ/singleroleaggregator.h
|
||||
include/StatusQ/sumaggregator.h
|
||||
src/QClipboardProxy.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
|
@ -109,6 +112,9 @@ add_library(StatusQ SHARED
|
|||
src/statuswindow.cpp
|
||||
src/stringutilsinternal.cpp
|
||||
src/submodelproxymodel.cpp
|
||||
src/aggregator.cpp
|
||||
src/singleroleaggregator.cpp
|
||||
src/sumaggregator.cpp
|
||||
|
||||
# wallet
|
||||
src/wallet/managetokenscontroller.cpp
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class QAbstractItemModel;
|
||||
|
||||
class Aggregator : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
|
||||
Q_PROPERTY(QVariant value READ value NOTIFY valueChanged)
|
||||
|
||||
public:
|
||||
explicit Aggregator(QObject *parent = nullptr);
|
||||
|
||||
QAbstractItemModel* model() const;
|
||||
void setModel(QAbstractItemModel* model);
|
||||
|
||||
QVariant value() const;
|
||||
|
||||
signals:
|
||||
void modelChanged();
|
||||
void valueChanged();
|
||||
|
||||
protected slots:
|
||||
virtual QVariant calculateAggregation() = 0;
|
||||
|
||||
protected:
|
||||
void recalculate();
|
||||
virtual bool acceptRoles(const QVector<int>& roles) { return true; };
|
||||
|
||||
private:
|
||||
QAbstractItemModel* m_model = nullptr;
|
||||
QVariant m_value;
|
||||
|
||||
void connectToModel();
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "StatusQ/aggregator.h"
|
||||
|
||||
class SingleRoleAggregator : public Aggregator {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QByteArray roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged)
|
||||
|
||||
public:
|
||||
explicit SingleRoleAggregator(QObject *parent = nullptr);
|
||||
|
||||
const QByteArray& roleName() const;
|
||||
void setRoleName(const QByteArray &roleName);
|
||||
|
||||
signals:
|
||||
void roleNameChanged();
|
||||
|
||||
protected:
|
||||
bool acceptRoles(const QVector<int>& roles) override;
|
||||
bool roleExists() const;
|
||||
|
||||
private:
|
||||
QByteArray m_roleName;
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
#include "StatusQ/singleroleaggregator.h"
|
||||
|
||||
class SumAggregator : public SingleRoleAggregator {
|
||||
|
||||
public:
|
||||
explicit SumAggregator(QObject *parent = nullptr);
|
||||
|
||||
protected slots:
|
||||
QVariant calculateAggregation() override;
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
#include "StatusQ/aggregator.h"
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
Aggregator::Aggregator(QObject *parent)
|
||||
: QObject(parent){
|
||||
connect(this, &Aggregator::modelChanged, this, &Aggregator::recalculate);
|
||||
}
|
||||
|
||||
QAbstractItemModel* Aggregator::model() const {
|
||||
return m_model;
|
||||
}
|
||||
|
||||
void Aggregator::setModel(QAbstractItemModel* model) {
|
||||
if(m_model == model)
|
||||
return;
|
||||
|
||||
if(m_model)
|
||||
disconnect(m_model, nullptr, this, nullptr);
|
||||
|
||||
m_model = model;
|
||||
connectToModel();
|
||||
emit modelChanged();
|
||||
}
|
||||
|
||||
QVariant Aggregator::value() const {
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void Aggregator::recalculate()
|
||||
{
|
||||
auto newValue = calculateAggregation();
|
||||
|
||||
if (m_value == newValue)
|
||||
return;
|
||||
|
||||
m_value = newValue;
|
||||
emit valueChanged();
|
||||
}
|
||||
|
||||
void Aggregator:: connectToModel() {
|
||||
if(m_model) {
|
||||
connect(m_model, &QAbstractItemModel::rowsInserted, this, &Aggregator::recalculate);
|
||||
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &Aggregator::recalculate);
|
||||
connect(m_model, &QAbstractItemModel::modelReset, this, &Aggregator::recalculate);
|
||||
connect(m_model, &QAbstractItemModel::dataChanged, this,
|
||||
[this](auto&, auto&, const QVector<int>& roles) {
|
||||
if (this->acceptRoles(roles))
|
||||
this->recalculate();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
#include "StatusQ/statuswindow.h"
|
||||
#include "StatusQ/stringutilsinternal.h"
|
||||
#include "StatusQ/submodelproxymodel.h"
|
||||
#include "StatusQ/sumaggregator.h"
|
||||
|
||||
|
||||
#include "wallet/managetokenscontroller.h"
|
||||
|
@ -38,6 +39,7 @@ public:
|
|||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
|
||||
qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator");
|
||||
|
||||
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#include "StatusQ/singleroleaggregator.h"
|
||||
|
||||
SingleRoleAggregator::SingleRoleAggregator(QObject *parent)
|
||||
: Aggregator(parent) {}
|
||||
|
||||
const QByteArray& SingleRoleAggregator::roleName() const {
|
||||
return m_roleName;
|
||||
}
|
||||
|
||||
void SingleRoleAggregator::setRoleName(const QByteArray &roleName) {
|
||||
if (m_roleName == roleName)
|
||||
return;
|
||||
m_roleName = roleName;
|
||||
emit roleNameChanged();
|
||||
|
||||
recalculate();
|
||||
}
|
||||
|
||||
bool SingleRoleAggregator::acceptRoles(const QVector<int>& roles) {
|
||||
QHash<int, QByteArray> roleNames = model()->roleNames();
|
||||
for (const int role : roles) {
|
||||
if (roleNames.contains(role)) {
|
||||
QString roleName = QString::fromUtf8(roleNames[role]);
|
||||
|
||||
// Check if the role name is equal to the expected one:
|
||||
if (roleName == m_roleName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, none of the roles match m_roleName
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SingleRoleAggregator::roleExists() const {
|
||||
return !model()->roleNames().keys(m_roleName).empty();
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
#include "StatusQ/sumaggregator.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
SumAggregator::SumAggregator(QObject *parent)
|
||||
: SingleRoleAggregator(parent) {
|
||||
recalculate();
|
||||
}
|
||||
|
||||
QVariant SumAggregator::calculateAggregation() {
|
||||
// Check if m_model exists and role name is initialized
|
||||
if (!model() || roleName().isEmpty())
|
||||
return 0.0;
|
||||
|
||||
// Check if m_roleName is part of the roles of the model
|
||||
QHash<int, QByteArray> roles = model()->roleNames();
|
||||
if (!roleExists()) {
|
||||
qWarning() << "Provided role name does not exist in the current model";
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Do the aggregation
|
||||
double total = 0.0;
|
||||
int rows = model()->rowCount();
|
||||
int role = roles.key(roleName());
|
||||
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
QModelIndex index = model()->index(row, 0);
|
||||
QVariant value = model()->data(index, role);
|
||||
|
||||
bool ok;
|
||||
total += value.toDouble(&ok);
|
||||
|
||||
if (!ok)
|
||||
qWarning() << "Unsupported type for given role (not convertible to double)";
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
|
@ -52,3 +52,15 @@ add_test(NAME LeftJoinModelTest COMMAND LeftJoinModelTest)
|
|||
add_executable(SubmodelProxyModelTest tst_SubmodelProxyModel.cpp)
|
||||
target_link_libraries(SubmodelProxyModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
|
||||
add_test(NAME SubmodelProxyModelTest COMMAND SubmodelProxyModelTest)
|
||||
|
||||
add_executable(AggregatorTest tst_Aggregator.cpp)
|
||||
target_link_libraries(AggregatorTest PRIVATE Qt5::Test StatusQ)
|
||||
add_test(NAME AggregatorTest COMMAND AggregatorTest)
|
||||
|
||||
add_executable(SingleRoleAggregatorTest tst_SingleRoleAggregator.cpp)
|
||||
target_link_libraries(SingleRoleAggregatorTest PRIVATE Qt5::Test StatusQ)
|
||||
add_test(NAME SingleRoleAggregatorTest COMMAND SingleRoleAggregatorTest)
|
||||
|
||||
add_executable(SumAggregatorTest tst_SumAggregator.cpp)
|
||||
target_link_libraries(SumAggregatorTest PRIVATE Qt5::Test StatusQ)
|
||||
add_test(NAME SumAggregatorTest COMMAND SumAggregatorTest)
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
#include <QtTest>
|
||||
#include <QAbstractListModel>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "StatusQ/aggregator.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO: To be removed once issue #12843 is resolved and we have a testing utils
|
||||
class TestSourceModel : public QAbstractListModel {
|
||||
|
||||
public:
|
||||
explicit TestSourceModel(QList<QPair<QString, QVariantList>> data)
|
||||
: m_data(std::move(data)) {
|
||||
m_roles.reserve(m_data.size());
|
||||
|
||||
for (auto i = 0; i < m_data.size(); i++)
|
||||
m_roles.insert(i, m_data.at(i).first.toUtf8());
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex& parent) const override {
|
||||
Q_ASSERT(m_data.size());
|
||||
return m_data.first().second.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex& index, int role) const override {
|
||||
if (!index.isValid() || role < 0 || role >= m_data.size())
|
||||
return {};
|
||||
|
||||
const auto row = index.row();
|
||||
|
||||
if (role >= m_data.length() || row >= m_data.at(0).second.length())
|
||||
return {};
|
||||
|
||||
return m_data.at(role).second.at(row);
|
||||
}
|
||||
|
||||
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
|
||||
beginInsertRows(parent, row, row + count - 1);
|
||||
m_data.insert(row, QPair<QString, QVariantList>());
|
||||
endInsertRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
void update(int index, int role, QVariant value) {
|
||||
Q_ASSERT(role < m_data.size() && index < m_data[role].second.size());
|
||||
m_data[role].second[index].setValue(std::move(value));
|
||||
|
||||
emit dataChanged(this->index(index, 0), this->index(index, 0), { role });
|
||||
}
|
||||
|
||||
void remove(int index) {
|
||||
beginRemoveRows(QModelIndex{}, index, index);
|
||||
|
||||
for (int i = 0; i < m_data.size(); i++) {
|
||||
auto& roleVariantList = m_data[i].second;
|
||||
Q_ASSERT(index < roleVariantList.size());
|
||||
roleVariantList.removeAt(index);
|
||||
}
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
return m_roles;
|
||||
}
|
||||
|
||||
private:
|
||||
QList<QPair<QString, QVariantList>> m_data;
|
||||
QHash<int, QByteArray> m_roles;
|
||||
};
|
||||
|
||||
class ChildAggregator : public Aggregator {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChildAggregator(QObject *parent = nullptr) {}
|
||||
|
||||
protected slots:
|
||||
QVariant calculateAggregation() override {
|
||||
return {counter++};
|
||||
}
|
||||
|
||||
private:
|
||||
int counter = 0;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class TestAggregator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QString m_roleNameWarningText = "Provided role name does not exist in the current model";
|
||||
QString m_unsuportedTypeWarningText = "Unsupported type for given role (not convertible to double)";
|
||||
|
||||
private slots:
|
||||
void testModel() {
|
||||
ChildAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "0.123", "0.0000015", "1.45", "25.45221001" }}
|
||||
});
|
||||
QSignalSpy modelChangedSpy(&aggregator, &Aggregator::modelChanged);
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
|
||||
// Test 1: Real model
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(aggregator.model(), &sourceModel);
|
||||
QCOMPARE(modelChangedSpy.count(), 1);
|
||||
QCOMPARE(valueChangedSpy.count(), 1);
|
||||
|
||||
// Test 2: Non existing model
|
||||
aggregator.setModel(nullptr);
|
||||
QCOMPARE(aggregator.model(), nullptr);
|
||||
QCOMPARE(modelChangedSpy.count(), 2);
|
||||
QCOMPARE(valueChangedSpy.count(), 2);
|
||||
}
|
||||
|
||||
void testCalculateAggregationTrigger() {
|
||||
ChildAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { 0.123, 1.0, 1.45, 25.45 }}
|
||||
});
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
int valueChangedSpyCount = 0;
|
||||
|
||||
// Test 1 - Initial:
|
||||
aggregator.setModel(&sourceModel);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
|
||||
// Test 2 - Delete row:
|
||||
sourceModel.remove(0);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
|
||||
// Test 3 - Update value row:
|
||||
sourceModel.update(2, 1, 26.45);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestAggregator)
|
||||
#include "tst_Aggregator.moc"
|
|
@ -0,0 +1,121 @@
|
|||
#include <QtTest>
|
||||
#include <QAbstractListModel>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "StatusQ/singleroleaggregator.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO: To be removed once issue #12843 is resolved and we have a testing utils
|
||||
class TestSourceModel : public QAbstractListModel {
|
||||
|
||||
public:
|
||||
explicit TestSourceModel(QList<QPair<QString, QVariantList>> data)
|
||||
: m_data(std::move(data)) {
|
||||
m_roles.reserve(m_data.size());
|
||||
|
||||
for (auto i = 0; i < m_data.size(); i++)
|
||||
m_roles.insert(i, m_data.at(i).first.toUtf8());
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex& parent) const override {
|
||||
Q_ASSERT(m_data.size());
|
||||
return m_data.first().second.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex& index, int role) const override {
|
||||
if (!index.isValid() || role < 0 || role >= m_data.size())
|
||||
return {};
|
||||
|
||||
const auto row = index.row();
|
||||
|
||||
if (role >= m_data.length() || row >= m_data.at(0).second.length())
|
||||
return {};
|
||||
|
||||
return m_data.at(role).second.at(row);
|
||||
}
|
||||
|
||||
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
|
||||
beginInsertRows(parent, row, row + count - 1);
|
||||
m_data.insert(row, QPair<QString, QVariantList>());
|
||||
endInsertRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
void update(int index, int role, QVariant value) {
|
||||
Q_ASSERT(role < m_data.size() && index < m_data[role].second.size());
|
||||
m_data[role].second[index].setValue(std::move(value));
|
||||
|
||||
emit dataChanged(this->index(index, 0), this->index(index, 0), { role });
|
||||
}
|
||||
|
||||
void remove(int index) {
|
||||
beginRemoveRows(QModelIndex{}, index, index);
|
||||
|
||||
for (int i = 0; i < m_data.size(); i++) {
|
||||
auto& roleVariantList = m_data[i].second;
|
||||
Q_ASSERT(index < roleVariantList.size());
|
||||
roleVariantList.removeAt(index);
|
||||
}
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
return m_roles;
|
||||
}
|
||||
|
||||
private:
|
||||
QList<QPair<QString, QVariantList>> m_data;
|
||||
QHash<int, QByteArray> m_roles;
|
||||
};
|
||||
|
||||
class ChildSingleRoleAggregator : public SingleRoleAggregator {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChildSingleRoleAggregator(QObject *parent = nullptr) {}
|
||||
|
||||
protected slots:
|
||||
QVariant calculateAggregation() override { return {}; }
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class TestSingleRoleAggregator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QString m_roleNameWarningText = "Provided role name does not exist in the current model";
|
||||
|
||||
private slots:
|
||||
|
||||
void testRoleName() {
|
||||
ChildSingleRoleAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "0.123", "0.0000015", "1.45", "25.45221001" }}
|
||||
});
|
||||
QSignalSpy roleNameSpy(&aggregator, &SingleRoleAggregator::roleNameChanged);
|
||||
|
||||
// Test 1 - Assign role name but model is nullptr
|
||||
aggregator.setRoleName("TestRole");
|
||||
QCOMPARE(aggregator.roleName(), QString("TestRole"));
|
||||
QCOMPARE(roleNameSpy.count(), 1);
|
||||
|
||||
// Test 2 - New role but doesn't exist in the current model
|
||||
aggregator.setModel(&sourceModel);
|
||||
aggregator.setRoleName("TestRole2");
|
||||
QCOMPARE(aggregator.roleName(), QString("TestRole2"));
|
||||
QCOMPARE(roleNameSpy.count(), 2);
|
||||
|
||||
// Test 3 - New role existing in the current model
|
||||
aggregator.setRoleName("balance");
|
||||
QCOMPARE(aggregator.roleName(), QString("balance"));
|
||||
QCOMPARE(roleNameSpy.count(), 3);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSingleRoleAggregator)
|
||||
#include "tst_SingleRoleAggregator.moc"
|
|
@ -0,0 +1,258 @@
|
|||
#include <QtTest>
|
||||
#include <QAbstractListModel>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "StatusQ/sumaggregator.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO: To be removed once issue #12843 is resolved and we have a testing utils
|
||||
class TestSourceModel : public QAbstractListModel {
|
||||
|
||||
public:
|
||||
explicit TestSourceModel(QList<QPair<QString, QVariantList>> data)
|
||||
: m_data(std::move(data)) {
|
||||
m_roles.reserve(m_data.size());
|
||||
|
||||
for (auto i = 0; i < m_data.size(); i++)
|
||||
m_roles.insert(i, m_data.at(i).first.toUtf8());
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex& parent) const override {
|
||||
Q_ASSERT(m_data.size());
|
||||
return m_data.first().second.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex& index, int role) const override {
|
||||
if (!index.isValid() || role < 0 || role >= m_data.size())
|
||||
return {};
|
||||
|
||||
const auto row = index.row();
|
||||
|
||||
if (role >= m_data.length() || row >= m_data.at(0).second.length())
|
||||
return {};
|
||||
|
||||
return m_data.at(role).second.at(row);
|
||||
}
|
||||
|
||||
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
|
||||
beginInsertRows(parent, row, row + count - 1);
|
||||
m_data.insert(row, QPair<QString, QVariantList>());
|
||||
endInsertRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
void update(int index, int role, QVariant value) {
|
||||
Q_ASSERT(role < m_data.size() && index < m_data[role].second.size());
|
||||
m_data[role].second[index].setValue(std::move(value));
|
||||
|
||||
emit dataChanged(this->index(index, 0), this->index(index, 0), { role });
|
||||
}
|
||||
|
||||
void remove(int index) {
|
||||
beginRemoveRows(QModelIndex{}, index, index);
|
||||
|
||||
for (int i = 0; i < m_data.size(); i++) {
|
||||
auto& roleVariantList = m_data[i].second;
|
||||
Q_ASSERT(index < roleVariantList.size());
|
||||
roleVariantList.removeAt(index);
|
||||
}
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
return m_roles;
|
||||
}
|
||||
|
||||
private:
|
||||
QList<QPair<QString, QVariantList>> m_data;
|
||||
QHash<int, QByteArray> m_roles;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class TestSumAggregator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QString m_roleNameWarningText = "Provided role name does not exist in the current model";
|
||||
QString m_unsuportedTypeWarningText = "Unsupported type for given role (not convertible to double)";
|
||||
|
||||
private slots:
|
||||
void testEmpty() {
|
||||
SumAggregator aggregator;
|
||||
QCOMPARE(aggregator.value(), 0.0);
|
||||
}
|
||||
|
||||
void testModel() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "0.123", "0.0000015", "1.45", "25.45221001" }}
|
||||
});
|
||||
QSignalSpy modelChangedSpy(&aggregator, &Aggregator::modelChanged);
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
|
||||
// Test 1: Real model
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(aggregator.model(), &sourceModel);
|
||||
QCOMPARE(modelChangedSpy.count(), 1);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
|
||||
// Test 2: Non existing model
|
||||
aggregator.setModel(nullptr);
|
||||
QCOMPARE(aggregator.model(), nullptr);
|
||||
QCOMPARE(modelChangedSpy.count(), 2);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
}
|
||||
|
||||
void testRoleName() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "0.123", "0.0000015", "1.45", "25.45221001" }}
|
||||
});
|
||||
QSignalSpy roleNameSpy(&aggregator, &SingleRoleAggregator::roleNameChanged);
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
|
||||
// Test 1 - Assign role name but model is nullptr
|
||||
aggregator.setRoleName("TestRole");
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_roleNameWarningText.toUtf8());
|
||||
QCOMPARE(aggregator.roleName(), QString("TestRole"));
|
||||
QCOMPARE(roleNameSpy.count(), 1);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
|
||||
// Test 2 - New role but doesn't exist in the current model
|
||||
aggregator.setModel(&sourceModel);
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_roleNameWarningText.toUtf8());
|
||||
aggregator.setRoleName("TestRole2");
|
||||
QCOMPARE(aggregator.roleName(), QString("TestRole2"));
|
||||
QCOMPARE(roleNameSpy.count(), 2);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
|
||||
// Test 3 - New role existing in the current model
|
||||
aggregator.setRoleName("balance");
|
||||
QCOMPARE(aggregator.roleName(), QString("balance"));
|
||||
QCOMPARE(roleNameSpy.count(), 3);
|
||||
QCOMPARE(valueChangedSpy.count(), 1);
|
||||
}
|
||||
|
||||
void testStringTypeValue() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "0.123", "1", "1.45", "25.45" }}
|
||||
});
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
int valueChangedSpyCount = 0;
|
||||
|
||||
// Test 1 - Initial:
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
aggregator.setRoleName("balance");
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.023);
|
||||
|
||||
// Test 2 - Delete row:
|
||||
sourceModel.remove(0);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 27.9);
|
||||
|
||||
// Test 3 - Update value row:
|
||||
sourceModel.update(2, 1, "26.45");
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.9);
|
||||
|
||||
// Test 4 - Update value row but other role, not `balance`, so it will not trigger a value change:
|
||||
sourceModel.update(2, 0, "52");
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.9);
|
||||
}
|
||||
|
||||
void testFloatTypeValue() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { 0.123, 1.0, 1.45, 25.45 }}
|
||||
});
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
int valueChangedSpyCount = 0;
|
||||
|
||||
// Test 1 - Initial:
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
aggregator.setRoleName("balance");
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.023);
|
||||
|
||||
// Test 2 - Delete row:
|
||||
sourceModel.remove(0);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 27.9);
|
||||
|
||||
// Test 3 - Update value row:
|
||||
sourceModel.update(2, 1, 26.45);
|
||||
valueChangedSpyCount++;
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.9);
|
||||
|
||||
// Test 4 - Update value row but other role, not `balance`, so it will not trigger a value change:
|
||||
sourceModel.update(2, 0, "52");
|
||||
QCOMPARE(valueChangedSpy.count(), valueChangedSpyCount);
|
||||
QCOMPARE(aggregator.value().toDouble(), 28.9);
|
||||
}
|
||||
|
||||
void testStringUnsupportedTypeValue() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13" }},
|
||||
{ "balance", { "aa", "bb" }}
|
||||
});
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_unsuportedTypeWarningText.toUtf8());
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_unsuportedTypeWarningText.toUtf8());
|
||||
aggregator.setRoleName("balance");
|
||||
|
||||
// Value didn't change, it was an unsuported type!
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
QCOMPARE(aggregator.value().toDouble(), 0);
|
||||
}
|
||||
|
||||
void testUnsupportedTypeValue() {
|
||||
SumAggregator aggregator;
|
||||
TestSourceModel sourceModel({
|
||||
{ "chainId", { "12", "13" }},
|
||||
{ "balance", { QByteArray(), QByteArray() }}
|
||||
});
|
||||
QSignalSpy valueChangedSpy(&aggregator, &Aggregator::valueChanged);
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_unsuportedTypeWarningText.toUtf8());
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
m_unsuportedTypeWarningText.toUtf8());
|
||||
aggregator.setRoleName("balance");
|
||||
|
||||
// Value didn't change, it was an unsuported type!
|
||||
QCOMPARE(valueChangedSpy.count(), 0);
|
||||
QCOMPARE(aggregator.value().toDouble(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSumAggregator)
|
||||
#include "tst_SumAggregator.moc"
|
Loading…
Reference in New Issue