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/statuswindow.h
|
||||||
include/StatusQ/stringutilsinternal.h
|
include/StatusQ/stringutilsinternal.h
|
||||||
include/StatusQ/submodelproxymodel.h
|
include/StatusQ/submodelproxymodel.h
|
||||||
|
include/StatusQ/aggregator.h
|
||||||
|
include/StatusQ/singleroleaggregator.h
|
||||||
|
include/StatusQ/sumaggregator.h
|
||||||
src/QClipboardProxy.cpp
|
src/QClipboardProxy.cpp
|
||||||
src/leftjoinmodel.cpp
|
src/leftjoinmodel.cpp
|
||||||
src/modelutilsinternal.cpp
|
src/modelutilsinternal.cpp
|
||||||
|
@ -109,6 +112,9 @@ add_library(StatusQ SHARED
|
||||||
src/statuswindow.cpp
|
src/statuswindow.cpp
|
||||||
src/stringutilsinternal.cpp
|
src/stringutilsinternal.cpp
|
||||||
src/submodelproxymodel.cpp
|
src/submodelproxymodel.cpp
|
||||||
|
src/aggregator.cpp
|
||||||
|
src/singleroleaggregator.cpp
|
||||||
|
src/sumaggregator.cpp
|
||||||
|
|
||||||
# wallet
|
# wallet
|
||||||
src/wallet/managetokenscontroller.cpp
|
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/statuswindow.h"
|
||||||
#include "StatusQ/stringutilsinternal.h"
|
#include "StatusQ/stringutilsinternal.h"
|
||||||
#include "StatusQ/submodelproxymodel.h"
|
#include "StatusQ/submodelproxymodel.h"
|
||||||
|
#include "StatusQ/sumaggregator.h"
|
||||||
|
|
||||||
|
|
||||||
#include "wallet/managetokenscontroller.h"
|
#include "wallet/managetokenscontroller.h"
|
||||||
|
@ -38,6 +39,7 @@ public:
|
||||||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||||
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
|
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
|
||||||
|
qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator");
|
||||||
|
|
||||||
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);
|
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)
|
add_executable(SubmodelProxyModelTest tst_SubmodelProxyModel.cpp)
|
||||||
target_link_libraries(SubmodelProxyModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
|
target_link_libraries(SubmodelProxyModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
|
||||||
add_test(NAME SubmodelProxyModelTest COMMAND SubmodelProxyModelTest)
|
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