Commit 8bb07aa5 authored by Volker Krause's avatar Volker Krause
Browse files

Add survey targeting expression parser/evaluator

parent d971549b
......@@ -105,6 +105,9 @@ endif()
add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_ASCII)
find_package(FLEX REQUIRED)
find_package(BISON REQUIRED)
find_package(Php)
set_package_properties(Php PROPERTIES URL "http://php.net" TYPE RECOMMENDED PURPOSE "Syntax checking of PHP server code.")
find_package(PhpUnit)
......
......@@ -24,6 +24,8 @@ uf_add_test(providertest UserFeedbackCore Qt5::Gui)
uf_add_test(feedbackconfigtest UserFeedbackWidgets)
uf_add_test(surveytargetexpressiontest.cpp UserFeedbackCommon)
if(TARGET UserFeedbackConsole)
uf_add_test(producttest UserFeedbackConsole)
uf_add_test(schematemplatetest.cpp UserFeedbackConsole)
......
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <common/surveytargetexpression.h>
#include <common/surveytargetexpressionevaluator.h>
#include <common/surveytargetexpressionparser.h>
#include <QDebug>
#include <QtTest/qtest.h>
#include <QObject>
using namespace UserFeedback;
class SurveyTargetExpressionTest: public QObject, public SurveyTargetExpressionDataProvider
{
Q_OBJECT
private:
QHash<QString, QVariant> m_sourceData;
QVariant sourceData(const QString &sourceName) const override
{
return m_sourceData.value(sourceName);
}
private slots:
void testValidParse_data()
{
QTest::addColumn<QString>("input");
QTest::newRow("01") << QStringLiteral("true == 12.3");
QTest::newRow("02") << QStringLiteral("42 >= false");
QTest::newRow("03") << QStringLiteral("(42 >= 12.3)");
QTest::newRow("04") << QStringLiteral("(42 >= 12.3) && (42 < 23)");
QTest::newRow("05") << QStringLiteral("(42 >= 12.3 && 42 < 23) || true != false");
QTest::newRow("06") << QStringLiteral("42 >= 12.3E-1 && 42 < 23.1e12 || true != false && -3.2 == .3");
QTest::newRow("07") << QStringLiteral("\"a\" == \"\\\"b\\\"\"");
QTest::newRow("08") << QStringLiteral("\"true\" == \"\" || \"&&\" != \"()\"");
QTest::newRow("09") << QStringLiteral("usageTime.value > 3600");
QTest::newRow("10") << QStringLiteral("3600 <= usageTime.value");
QTest::newRow("11") << QStringLiteral("startCount.value != usageTime.value");
QTest::newRow("12") << QStringLiteral("screens[0].dpi <= 240");
QTest::newRow("13") << QStringLiteral("views[\"view1\"].ratio > 1");
}
void testValidParse()
{
QFETCH(QString, input);
SurveyTargetExpressionParser p;
QVERIFY(p.parse(input));
QVERIFY(p.expression());
qDebug() << p.expression();
}
void testInvalidParse_data()
{
QTest::addColumn<QString>("input");
QTest::newRow("01") << QStringLiteral("true && 12.3");
QTest::newRow("02") << QStringLiteral("42 || false");
QTest::newRow("03") << QStringLiteral("(42)");
QTest::newRow("04") << QStringLiteral("true == 12.3 >= 24 < (false)");
QTest::newRow("05") << QStringLiteral("screen[0] == 4");
QTest::newRow("06") << QStringLiteral("screen[false] == 4");
QTest::newRow("07") << QStringLiteral("screen[false].dpi == 4");
QTest::newRow("08") << QStringLiteral("screen[].dpi == 4");
QTest::newRow("08") << QStringLiteral("screen[].dpi == 4");
QTest::newRow("09") << QStringLiteral("screen[\"key\"] == 4");
QTest::newRow("10") << QStringLiteral("\"\\\"\" == \"\\\"");
}
void testInvalidParse()
{
QFETCH(QString, input);
SurveyTargetExpressionParser p;
QVERIFY(!p.parse(input));
}
void testEval_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<bool>("expected");
QTest::newRow("01") << QStringLiteral("1 >= 2") << false;
QTest::newRow("02") << QStringLiteral("1 < 2.0") << true;
QTest::newRow("03") << QStringLiteral("true != false") << true;
QTest::newRow("04") << QStringLiteral("\"abc\" == \"abc\"") << true;
QTest::newRow("05") << QStringLiteral("1.0 > 1.5 || 1.0 <= 1.5") << true;
QTest::newRow("06") << QStringLiteral("1.0 >= 1.5 && 1.0 < 1.5") << false;
QTest::newRow("07") << QStringLiteral("3600 <= usageTime.value") << true;
QTest::newRow("08") << QStringLiteral("3600 <= usageTime.value || 42 < lazy_eval.non_existing") << true;
QTest::newRow("09") << QStringLiteral("screens[1].dpi <= 240") << true;
QTest::newRow("10") << QStringLiteral("screens[2].dpi <= 240") << false;
QTest::newRow("11") << QStringLiteral("screens.size == 2") << true;
QTest::newRow("12") << QStringLiteral("1 == \"1\"") << false;
QTest::newRow("13") << QStringLiteral("\"1\" == 1") << false;
QTest::newRow("14") << QStringLiteral("screens[0].size == 1920") << true;
QTest::newRow("15") << QStringLiteral("views[\"view2\"].ratio > 0.35") << true;
QTest::newRow("16") << QStringLiteral("views[\"view3\"].size == \"???\"") << true;
QTest::newRow("17") << QStringLiteral("views[\"view4\"].ratio > 0.35") << false;
QTest::newRow("18") << QStringLiteral("views.size == 3") << true;
QTest::newRow("19") << QStringLiteral("test.string == \"a\\\"b\\nc\\\\d\\te\\\\f\"") << true;
QTest::newRow("20") << QStringLiteral("views[\"\"].ratio > 0.35") << false;
QTest::newRow("21") << QStringLiteral("\"\" == \"\"") << true;
QTest::newRow("22") << QStringLiteral("views.view1 == 2") << false;
QTest::newRow("23") << QStringLiteral("views.non_existing == false") << false;
QTest::newRow("24") << QStringLiteral("1 == 1 || 2 == 2 && 3 != 3") << true;
QTest::newRow("25") << QStringLiteral("(1 == 1 || 2 == 2) && 3 != 3") << false;
QTest::newRow("26") << QStringLiteral("1 == 1 && 2 == 2 || 3 != 3") << true;
QTest::newRow("27") << QStringLiteral("1 == 1 && 2 == 2 || 3 == 3 && 4!=4") << true;
}
void testEval()
{
QFETCH(QString, input);
QFETCH(bool, expected);
QVariantMap m;
m.insert(QLatin1String("value"), 3600);
m_sourceData.insert(QLatin1String("usageTime"), m);
m.clear();
m.insert(QLatin1String("dpi"), 200);
m.insert(QLatin1String("size"), 1920);
QVariantList l;
l.push_back(m);
l.push_back(m);
m_sourceData.insert(QLatin1String("screens"), l);
m.clear();
m.insert(QLatin1String("ratio"), 0.4);
m.insert(QLatin1String("size"), QLatin1String("???"));
QVariantMap m2;
m2.insert(QLatin1String("view1"), m);
m2.insert(QLatin1String("view2"), m);
m2.insert(QLatin1String("view3"), m);
m_sourceData.insert(QLatin1String("views"), m2);
m.clear();
m.insert(QLatin1String("string"), QLatin1String("a\"b\nc\\d\te\\f"));
m_sourceData.insert(QLatin1String("test"), m);
SurveyTargetExpressionParser p;
QVERIFY(p.parse(input));
QVERIFY(p.expression());
qDebug() << p.expression();
SurveyTargetExpressionEvaluator eval;
eval.setDataProvider(this);
QCOMPARE(eval.evaluate(p.expression()), expected);
}
};
QTEST_MAIN(SurveyTargetExpressionTest)
#include "surveytargetexpressiontest.moc"
add_subdirectory(common)
add_subdirectory(provider)
add_subdirectory(server)
add_subdirectory(testserver)
......
set(common_src
surveytargetexpression.cpp
surveytargetexpressionevaluator.cpp
surveytargetexpressionparser.cpp
)
flex_target(surveytargetexpressionscanner
surveytargetexpressionlexer.l
${CMAKE_CURRENT_BINARY_DIR}/surveytargetexpressionlexer.cpp
COMPILE_FLAGS --header-file=${CMAKE_CURRENT_BINARY_DIR}/surveytargetexpressionscanner.h
)
bison_target(surveytargetexpressionparser
surveytargetexpressionparser.y
${CMAKE_CURRENT_BINARY_DIR}/surveytargetexpressionparser_p.cpp
COMPILE_FLAGS --defines=${CMAKE_CURRENT_BINARY_DIR}/surveytargetexpressionparser_p.h
)
add_flex_bison_dependency(surveytargetexpressionscanner surveytargetexpressionparser)
add_library(UserFeedbackCommon STATIC
${common_src}
${BISON_surveytargetexpressionparser_OUTPUTS}
${FLEX_surveytargetexpressionscanner_OUTPUTS}
)
target_include_directories(UserFeedbackCommon PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/..>")
if(Qt5Core_FOUND)
target_link_libraries(UserFeedbackCommon LINK_PRIVATE Qt5::Core)
else()
target_link_libraries(UserFeedbackCommon LINK_PRIVATE ${QT_QTCORE_LIBS})
endif()
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "surveytargetexpression.h"
using namespace UserFeedback;
SurveyTargetExpression::SurveyTargetExpression(const QVariant &value)
: m_type(Value)
, m_value(value)
{
}
SurveyTargetExpression::SurveyTargetExpression(const QString& source, const QVariant &index, const QString& elem)
: m_value(index)
, m_source(source)
, m_sourceElement(elem)
{
if (index.type() == QMetaType::Int)
m_type = ListElement;
else if (index.type() == QMetaType::QString)
m_type = MapElement;
else
m_type = ScalarElement;
}
SurveyTargetExpression::SurveyTargetExpression(Type type, SurveyTargetExpression* left, SurveyTargetExpression* right)
: m_type(type)
, m_left(left)
, m_right(right)
{
}
SurveyTargetExpression::~SurveyTargetExpression()
{
}
SurveyTargetExpression::Type SurveyTargetExpression::type() const
{
return m_type;
}
QVariant SurveyTargetExpression::value() const
{
return m_value;
}
QString SurveyTargetExpression::source() const
{
return m_source;
}
QString SurveyTargetExpression::sourceElement() const
{
return m_sourceElement;
}
SurveyTargetExpression* SurveyTargetExpression::left() const
{
return m_left.get();
}
SurveyTargetExpression* SurveyTargetExpression::right() const
{
return m_right.get();
}
QDebug operator<<(QDebug debug, SurveyTargetExpression* expr)
{
if (!expr) {
debug << "(null)";
return debug;
}
switch (expr->type()) {
case SurveyTargetExpression::Value:
debug << expr->value();
break;
case SurveyTargetExpression::ScalarElement:
debug.nospace() << expr->source() << "." << expr->sourceElement();
break;
case SurveyTargetExpression::ListElement:
debug.nospace() << expr->source() << "[" << expr->value().toInt() << "]." << expr->sourceElement();
break;
case SurveyTargetExpression::MapElement:
debug.nospace() << expr->source() << "[" << expr->value().toString() << "]." << expr->sourceElement();
break;
case SurveyTargetExpression::OpLogicAnd:
debug.nospace() << "(" << expr->left() << " && " << expr->right() << ")";
break;
case SurveyTargetExpression::OpLogicOr:
debug.nospace() << "(" << expr->left() << " || " << expr->right() << ")";
break;
case SurveyTargetExpression::OpEqual:
debug.nospace() << "[" << expr->left() << " == " << expr->right() << "]";
break;
case SurveyTargetExpression::OpNotEqual:
debug.nospace() << "[" << expr->left() << " != " << expr->right() << "]";
break;
case SurveyTargetExpression::OpGreater:
debug.nospace() << "[" << expr->left() << " > " << expr->right() << "]";
break;
case SurveyTargetExpression::OpGreaterEqual:
debug.nospace() << "[" << expr->left() << " >= " << expr->right() << "]";
break;
case SurveyTargetExpression::OpLess:
debug.nospace() << "[" << expr->left() << " < " << expr->right() << "]";
break;
case SurveyTargetExpression::OpLessEqual:
debug.nospace() << "[" << expr->left() << " <= " << expr->right() << "]";
break;
}
return debug;
}
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef USERFEEDBACK_SURVEYTARGETEXPRESSION_H
#define USERFEEDBACK_SURVEYTARGETEXPRESSION_H
#include <QDebug>
#include <QString>
#include <QVariant>
#include <memory>
namespace UserFeedback {
class SurveyTargetExpression
{
public:
enum Type {
Value,
ScalarElement,
ListElement,
MapElement,
OpLogicAnd,
OpLogicOr,
OpEqual,
OpNotEqual,
OpLess,
OpLessEqual,
OpGreater,
OpGreaterEqual
};
explicit SurveyTargetExpression(const QVariant &value);
explicit SurveyTargetExpression(const QString &source, const QVariant &index, const QString &elem);
explicit SurveyTargetExpression(Type type, SurveyTargetExpression *left, SurveyTargetExpression *right);
~SurveyTargetExpression();
Type type() const;
QVariant value() const;
QString source() const;
QString sourceElement() const;
SurveyTargetExpression* left() const;
SurveyTargetExpression* right() const;
private:
Type m_type;
QVariant m_value;
QString m_source;
QString m_sourceElement;
std::unique_ptr<SurveyTargetExpression> m_left;
std::unique_ptr<SurveyTargetExpression> m_right;
};
}
QDebug operator<<(QDebug debug, UserFeedback::SurveyTargetExpression *expr);
#endif
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "surveytargetexpressionevaluator.h"
#include "surveytargetexpression.h"
using namespace UserFeedback;
SurveyTargetExpressionEvaluator::SurveyTargetExpressionEvaluator()
: m_provider(nullptr)
{
}
SurveyTargetExpressionEvaluator::~SurveyTargetExpressionEvaluator()
{
}
void SurveyTargetExpressionEvaluator::setDataProvider(SurveyTargetExpressionDataProvider* provider)
{
m_provider = provider;
}
bool SurveyTargetExpressionEvaluator::evaluate(SurveyTargetExpression* expression)
{
// logical operations
switch (expression->type()) {
case SurveyTargetExpression::OpLogicAnd:
return evaluate(expression->left()) && evaluate(expression->right());
case SurveyTargetExpression::OpLogicOr:
return evaluate(expression->left()) || evaluate(expression->right());
default:
break;
}
// comparisson operations:
const auto lhs = value(expression->left());
const auto rhs = value(expression->right());
if (lhs.type() == QVariant::Invalid || rhs.type() == QVariant::Invalid) // invalid element access can never succeed
return false;
if ((lhs.type() == QVariant::String && rhs.type() != QVariant::String)
|| (lhs.type() != QVariant::String && rhs.type() == QVariant::String))
return false; // strings can only be compared to strings
switch (expression->type()) {
case SurveyTargetExpression::OpEqual:
return lhs == rhs;
case SurveyTargetExpression::OpNotEqual:
return lhs != rhs;
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
case SurveyTargetExpression::OpGreater:
return lhs > rhs;
case SurveyTargetExpression::OpGreaterEqual:
return lhs >= rhs;
case SurveyTargetExpression::OpLess:
return lhs < rhs;
case SurveyTargetExpression::OpLessEqual:
return lhs <= rhs;
#else
case SurveyTargetExpression::OpGreater:
return lhs.toDouble() > rhs.toDouble();
case SurveyTargetExpression::OpGreaterEqual:
return lhs.toDouble() >= rhs.toDouble();
case SurveyTargetExpression::OpLess:
return lhs.toDouble() < rhs.toDouble();
case SurveyTargetExpression::OpLessEqual:
return lhs.toDouble() <= rhs.toDouble();
#endif
default:
break;
}
return false;
}
QVariant SurveyTargetExpressionEvaluator::value(SurveyTargetExpression* expression)
{
switch (expression->type()) {
case SurveyTargetExpression::Value:
return expression->value();
case SurveyTargetExpression::ScalarElement:
{
const auto v = value(expression->source());
if (v.canConvert<QVariantList>() && expression->sourceElement() == QLatin1String("size"))
return v.value<QVariantList>().size();
const auto m = v.toMap();
const auto it = m.find(expression->sourceElement());
if (it != m.end() && !it.value().canConvert<QVariantMap>())
return it.value();
if (expression->sourceElement() == QLatin1String("size"))
return m.size();
return QVariant();
}
case SurveyTargetExpression::ListElement:
{
const auto v = value(expression->source()).value<QVariantList>().value(expression->value().toInt());
return v.toMap().value(expression->sourceElement());
}
case SurveyTargetExpression::MapElement:
{
const auto v = value(expression->source()).toMap().value(expression->value().toString());
qDebug() << v << value(expression->source()).toMap() << expression->value().toString();
return v.toMap().value(expression->sourceElement());
}
default:
break;
}
Q_ASSERT(false);
return QVariant();
}
QVariant SurveyTargetExpressionEvaluator::value(const QString& source)
{
Q_ASSERT(m_provider);
const auto it = m_dataCache.constFind(source);
if (it != m_dataCache.constEnd())
return it.value();
const auto v = m_provider->sourceData(source);
m_dataCache.insert(source, v);
return v;
}
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef USERFEEDBACK_SURVEYTARGETEXPRESSIONEVALUATOR_H
#define USERFEEDBACK_SURVEYTARGETEXPRESSIONEVALUATOR_H
#include <QHash>
class QString;
class QVariant;
namespace UserFeedback {
class SurveyTargetExpression;
class SurveyTargetExpressionDataProvider