1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/test/chatlog/textformatter_test.cpp

539 lines
20 KiB
C++

/*
Copyright © 2017-2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "src/chatlog/textformatter.h"
#include <QtTest/QtTest>
#include <QString>
#include <ctime>
#include <functional>
#define PAIR_FORMAT(input, output) {QStringLiteral(input), QStringLiteral(output)}
using StringPair = QPair<QString, QString>;
static const StringPair TAGS[] {
PAIR_FORMAT("<b>", "</b>"),
PAIR_FORMAT("<i>", "</i>"),
PAIR_FORMAT("<u>", "</u>"),
PAIR_FORMAT("<s>", "</s>"),
PAIR_FORMAT("<font color=#595959><code>", "</code></font>"),
};
enum StyleType {
BOLD,
ITALIC,
UNDERLINE,
STRIKE,
CODE,
};
/**
* @brief The MarkdownToTags struct maps sequence of markdown symbols to HTML tags according this
* sequence
*/
struct MarkdownToTags
{
QString markdownSequence;
StringPair htmlTags;
};
static const QVector<MarkdownToTags> SINGLE_SIGN_MARKDOWN {
{QStringLiteral("*"), TAGS[StyleType::BOLD]},
{QStringLiteral("/"), TAGS[StyleType::ITALIC]},
{QStringLiteral("_"), TAGS[StyleType::UNDERLINE]},
{QStringLiteral("~"), TAGS[StyleType::STRIKE]},
{QStringLiteral("`"), TAGS[StyleType::CODE]},
};
static const QVector<MarkdownToTags> DOUBLE_SIGN_MARKDOWN {
{QStringLiteral("**"), TAGS[StyleType::BOLD]},
{QStringLiteral("//"), TAGS[StyleType::ITALIC]},
{QStringLiteral("__"), TAGS[StyleType::UNDERLINE]},
{QStringLiteral("~~"), TAGS[StyleType::STRIKE]},
};
static const QVector<MarkdownToTags> MULTI_SIGN_MARKDOWN {
{QStringLiteral("```"), TAGS[StyleType::CODE]},
};
/**
* @brief Creates single container from two
*/
template<class Container>
static Container concat(const Container& first, const Container& last)
{
Container result;
result.reserve(first.size() + last.size());
result.append(first);
result.append(last);
return result;
}
static const QVector<MarkdownToTags> ALL_MARKDOWN_TYPES = concat(concat(SINGLE_SIGN_MARKDOWN,
DOUBLE_SIGN_MARKDOWN),
MULTI_SIGN_MARKDOWN);
static const QVector<MarkdownToTags> SINGLE_AND_DOUBLE_MARKDOWN = concat(SINGLE_SIGN_MARKDOWN,
DOUBLE_SIGN_MARKDOWN);
// any markdown type must work for this data the same way
static const QVector<StringPair> COMMON_WORK_CASES {
PAIR_FORMAT("%1a%1", "%2%1a%1%3"),
PAIR_FORMAT("%1aa%1", "%2%1aa%1%3"),
PAIR_FORMAT("%1aaa%1", "%2%1aaa%1%3"),
// Must allow same formatting more than one time
PAIR_FORMAT("%1aaa%1 %1aaa%1", "%2%1aaa%1%3 %2%1aaa%1%3"),
};
static const QVector<StringPair> SINGLE_SIGN_WORK_CASES {
PAIR_FORMAT("a %1a%1", "a %2%1a%1%3"),
PAIR_FORMAT("%1a%1 a", "%2%1a%1%3 a"),
PAIR_FORMAT("a %1a%1 a", "a %2%1a%1%3 a"),
// "Lazy" matching
PAIR_FORMAT("%1aaa%1 aaa%1", "%2%1aaa%1%3 aaa%4"),
};
// only double-sign markdown must work for this data
static const QVector<StringPair> DOUBLE_SIGN_WORK_CASES {
// Must apply formatting to strings which contain reserved symbols
PAIR_FORMAT("%1aaa%2%1", "%3%1aaa%2%1%4"),
PAIR_FORMAT("%1%2aaa%1", "%3%1%2aaa%1%4"),
PAIR_FORMAT("%1aaa%2aaa%1", "%3%1aaa%2aaa%1%4"),
PAIR_FORMAT("%1%2%2aaa%1", "%3%1%2%2aaa%1%4"),
PAIR_FORMAT("%1aaa%2%2%1", "%3%1aaa%2%2%1%4"),
PAIR_FORMAT("%1aaa%2%2aaa%1", "%3%1aaa%2%2aaa%1%4"),
};
// only multi-sign markdown must work for this data
static const QVector<StringPair> MULTI_SIGN_WORK_CASES {
PAIR_FORMAT("%1int main()\n{ return 0;\n}%1", "%2%1"
"int main()\n{ return 0;\n}"
"%1%3"),
// allows new line and spaces to go right after ``` sequence
PAIR_FORMAT("%1\nint main()\n{ return 0;\n}\n%1", "%2%1"
"\nint main()\n{ return 0;\n}\n"
"%1%3"),
PAIR_FORMAT("%1 int main()\n{ return 0;\n} %1", "%2%1"
" int main()\n{ return 0;\n} "
"%1%3"),
};
// any type of markdown must fail for this data
static const QVector<QString> COMMON_EXCEPTIONS {
// No empty formatting string
QStringLiteral("%1%1"),
// Formatting string must be enclosed by whitespace symbols, newlines or message start/end
QStringLiteral("a%1aa%1a"), QStringLiteral("%1aa%1a"), QStringLiteral("a%1aa%1"),
QStringLiteral("a %1aa%1a"), QStringLiteral("a%1aa%1 a"),
QStringLiteral("a\n%1aa%1a"), QStringLiteral("a%1aa%1\na"),
};
static const QVector<QString> SINGLE_AND_DOUBLE_SIGN_EXCEPTIONS {
// Formatting text must not start/end with whitespace symbols
QStringLiteral("%1 %1"), QStringLiteral("%1 a%1"), QStringLiteral("%1a %1"),
// No newlines
QStringLiteral("%1\n%1"), QStringLiteral("%1aa\n%1"), QStringLiteral("%1\naa%1"),
QStringLiteral("%1aa\naa%1"),
};
// only single-sign markdown must fail for this data
static const QVector<QString> SINGLE_SIGN_EXCEPTIONS {
// Reserved symbols within formatting string are disallowed
QStringLiteral("%1aa%1a%1"), QStringLiteral("%1aa%1%1"), QStringLiteral("%1%1aa%1"),
QStringLiteral("%1%1%1"),
};
static const QVector<StringPair> MIXED_FORMATTING_SPECIAL_CASES {
// Must allow mixed formatting if there is no tag overlap in result
PAIR_FORMAT("aaa *aaa /aaa/ aaa*", "aaa <b>aaa <i>aaa</i> aaa</b>"),
PAIR_FORMAT("aaa *aaa /aaa* aaa/", "aaa *aaa <i>aaa* aaa</i>"),
};
#define MAKE_LINK(url) "<a href=\"" url "\">" url "</a>"
#define MAKE_WWW_LINK(url) "<a href=\"http://" url "\">" url "</a>"
static const QVector<QPair<QString, QString>> URL_CASES {
PAIR_FORMAT("https://github.com/qTox/qTox/issues/4233",
MAKE_LINK("https://github.com/qTox/qTox/issues/4233")),
PAIR_FORMAT("www.youtube.com", MAKE_WWW_LINK("www.youtube.com")),
PAIR_FORMAT("https://url.com/some*url/some*more*url/",
MAKE_LINK("https://url.com/some*url/some*more*url/")),
PAIR_FORMAT("https://url.com/some_url/some_more_url/",
MAKE_LINK("https://url.com/some_url/some_more_url/")),
PAIR_FORMAT("https://url.com/some~url/some~more~url/",
MAKE_LINK("https://url.com/some~url/some~more~url/")),
// Test case from issue #4275
PAIR_FORMAT("http://www.metacritic.com/game/pc/mass-effect-andromeda\n"
"http://www.metacritic.com/game/playstation-4/mass-effect-andromeda\n"
"http://www.metacritic.com/game/xbox-one/mass-effect-andromeda",
MAKE_LINK("http://www.metacritic.com/game/pc/mass-effect-andromeda") "\n"
MAKE_LINK("http://www.metacritic.com/game/playstation-4/mass-effect-andromeda") "\n"
MAKE_LINK("http://www.metacritic.com/game/xbox-one/mass-effect-andromeda")),
PAIR_FORMAT("http://site.com/part1/part2 "
"http://site.com/part3 and one more time "
"www.site.com/part1/part2",
MAKE_LINK("http://site.com/part1/part2") " "
MAKE_LINK("http://site.com/part3") " and one more time "
MAKE_WWW_LINK("www.site.com/part1/part2")),
PAIR_FORMAT("https://127.0.0.1/asd\n"
"https://ABCD:EF01:2345:6789:ABCD:EF01:2345:6789/\n"
"ftp://2001:DB8::8:800:200C:417A/\n"
"http://::1/\n"
"http://::/\n"
"https://127.0.0.1:8080/asd "
"https://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:8080/ "
"ftp://[2001:DB8::8:800:200C:417A]:21/ "
"http://[::1]:22/ "
"http://[::]:20/ ",
MAKE_LINK("https://127.0.0.1/asd") "\n"
MAKE_LINK("https://ABCD:EF01:2345:6789:ABCD:EF01:2345:6789/") "\n"
MAKE_LINK("ftp://2001:DB8::8:800:200C:417A/") "\n"
MAKE_LINK("http://::1/") "\n"
MAKE_LINK("http://::/") "\n"
MAKE_LINK("https://127.0.0.1:8080/asd") " "
MAKE_LINK("https://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:8080/") " "
MAKE_LINK("ftp://[2001:DB8::8:800:200C:417A]:21/") " "
MAKE_LINK("http://[::1]:22/") " "
MAKE_LINK("http://[::]:20/") " "
),
// Test case from issue #4853 (include unicode, ending brackets that are part of URL)
PAIR_FORMAT("https://ja.wikipedia.org/wiki/印章",
MAKE_LINK("https://ja.wikipedia.org/wiki/印章")),
PAIR_FORMAT("https://en.wikipedia.org/wiki/Seal_(East_Asia)",
MAKE_LINK("https://en.wikipedia.org/wiki/Seal_(East_Asia)")),
// Test cases from issue #4295 (exclude surrounding quotes, brackets, ending punctuation)
PAIR_FORMAT("(http://www.google.com)",
"(" MAKE_LINK("http://www.google.com") ")"),
PAIR_FORMAT("&quot;http://www.google.com&quot;",
"&quot;" MAKE_LINK("http://www.google.com") "&quot;"),
PAIR_FORMAT("http://www.google.com.",
MAKE_LINK("http://www.google.com") "."),
PAIR_FORMAT("http://www.google.com,",
MAKE_LINK("http://www.google.com") ","),
PAIR_FORMAT("http://www.google.com?",
MAKE_LINK("http://www.google.com") "?"),
PAIR_FORMAT("https://google.com?gfe_rd=cr",
MAKE_LINK("https://google.com?gfe_rd=cr")),
PAIR_FORMAT("[&quot;https://en.wikipedia.org/wiki/Seal_(East_Asia)&quot;]?",
"[&quot;" MAKE_LINK("https://en.wikipedia.org/wiki/Seal_(East_Asia)") "&quot;]?")
};
#undef PAIR_FORMAT
using MarkdownFunction = QString (*)(const QString&, bool);
using InputProcessor = std::function<QString(const QString&, const MarkdownToTags&)>;
using OutputProcessor = std::function<QString(const QString&, const MarkdownToTags&, bool)>;
/**
* @brief Testing cases where markdown must work
* @param applyMarkdown Function which is used to apply markdown
* @param markdownToTags Which markdown type to test
* @param testData Test data - string pairs "Source message - Message after formatting"
* @param showSymbols True if it is supposed to leave markdown symbols after formatting, false
* otherwise
* @param processInput Test data is a template, which must be expanded with concrete markdown
* symbols, everytime in different way. This function determines how to expand source message
* depending on user need
* @param processOutput Same as previous parameter but is applied to markdown output
*/
static void workCasesTest(MarkdownFunction applyMarkdown,
const QVector<MarkdownToTags>& markdownToTags,
const QVector<StringPair>& testData,
bool showSymbols,
InputProcessor processInput = nullptr,
OutputProcessor processOutput = nullptr)
{
for (const MarkdownToTags& mtt: markdownToTags) {
for (const StringPair& data: testData) {
const QString input = processInput != nullptr ? processInput(data.first, mtt)
: data.first;
QString output = processOutput != nullptr ? processOutput(data.second, mtt, showSymbols)
: data.second;
QString result = applyMarkdown(input, showSymbols);
QVERIFY(output == result);
}
}
}
/**
* @brief Testing cases where markdown must not to work
* @param applyMarkdown Function which is used to apply markdown
* @param markdownToTags Which markdown type to test
* @param exceptions Collection of "source message - markdown result" pairs representing cases
* where markdown must not to work
* @param showSymbols True if it is supposed to leave markdown symbols after formatting, false
* otherwise
*/
static void exceptionsTest(MarkdownFunction applyMarkdown,
const QVector<MarkdownToTags>& markdownToTags,
const QVector<QString>& exceptions,
bool showSymbols)
{
for (const MarkdownToTags& mtt: markdownToTags) {
for (const QString& e: exceptions) {
QString processedException = e.arg(mtt.markdownSequence);
QVERIFY(processedException == applyMarkdown(processedException, showSymbols));
}
}
}
/**
* @brief Testing some uncommon work cases
* @param applyMarkdown Function which is used to apply markdown
* @param pairs Collection of "source message - markdown result" pairs representing cases where
* markdown must not to work
*/
static void specialCasesTest(MarkdownFunction applyMarkdown,
const QVector<StringPair>& pairs)
{
for (const auto& p : pairs) {
QString result = applyMarkdown(p.first, false);
QVERIFY(p.second == result);
}
}
using UrlHighlightFunction = QString(*)(const QString&);
/**
* @brief Function for testing URL highlighting
* @param data Test data - map of "URL - HTML-wrapped URL"
*/
static void urlHighlightTest(UrlHighlightFunction function, const QVector<QPair<QString, QString>>& data)
{
for (const QPair<QString, QString>& p : data) {
QString result = function(p.first);
QVERIFY(p.second == result);
}
}
class TestTextFormatter : public QObject
{
Q_OBJECT
private slots:
void commonWorkCasesShowSymbols();
void commonWorkCasesHideSymbols();
void singleSignWorkCasesShowSymbols();
void singleSignWorkCasesHideSymbols();
void doubleSignWorkCasesShowSymbols();
void doubleSignWorkCasesHideSymbols();
void multiSignWorkCasesHideSymbols();
void multiSignWorkCasesShowSymbols();
void commonExceptionsShowSymbols();
void commonExceptionsHideSymbols();
void singleSignExceptionsShowSymbols();
void singleSignExceptionsHideSymbols();
void singleAndDoubleMarkdownExceptionsShowSymbols();
void singleAndDoubleMarkdownExceptionsHideSymbols();
void mixedFormattingSpecialCases();
void urlTest();
private:
const MarkdownFunction markdownFunction = applyMarkdown;
UrlHighlightFunction urlHighlightFunction = highlightURI;
};
static QString commonWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt)
{
return str.arg(mtt.markdownSequence);
}
static QString commonWorkCasesProcessOutput(const QString& str,
const MarkdownToTags& mtt,
bool showSymbols)
{
const StringPair& tags = mtt.htmlTags;
return str.arg(showSymbols ? mtt.markdownSequence : QString{}).arg(tags.first).arg(tags.second);
}
void TestTextFormatter::commonWorkCasesShowSymbols()
{
workCasesTest(markdownFunction,
ALL_MARKDOWN_TYPES,
COMMON_WORK_CASES,
true,
commonWorkCasesProcessInput,
commonWorkCasesProcessOutput);
}
void TestTextFormatter::commonWorkCasesHideSymbols()
{
workCasesTest(markdownFunction,
ALL_MARKDOWN_TYPES,
COMMON_WORK_CASES,
false,
commonWorkCasesProcessInput,
commonWorkCasesProcessOutput);
}
static QString singleSignWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt)
{
return str.arg(mtt.markdownSequence);
}
static QString singleSignWorkCasesProcessOutput(const QString& str,
const MarkdownToTags& mtt,
bool showSymbols)
{
const StringPair& tags = mtt.htmlTags;
return str.arg(showSymbols ? mtt.markdownSequence : "")
.arg(tags.first)
.arg(tags.second)
.arg(mtt.markdownSequence);
}
void TestTextFormatter::singleSignWorkCasesShowSymbols()
{
workCasesTest(markdownFunction,
SINGLE_SIGN_MARKDOWN,
SINGLE_SIGN_WORK_CASES,
true,
singleSignWorkCasesProcessInput,
singleSignWorkCasesProcessOutput);
}
void TestTextFormatter::singleSignWorkCasesHideSymbols()
{
workCasesTest(markdownFunction,
SINGLE_SIGN_MARKDOWN,
SINGLE_SIGN_WORK_CASES,
false,
singleSignWorkCasesProcessInput,
singleSignWorkCasesProcessOutput);
}
static QString doubleSignWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt)
{
return str.arg(mtt.markdownSequence).arg(mtt.markdownSequence[0]);
}
static QString doubleSignWorkCasesProcessOutput(const QString& str,
const MarkdownToTags& mtt,
bool showSymbols)
{
const StringPair& tags = mtt.htmlTags;
return str.arg(showSymbols ? mtt.markdownSequence : "")
.arg(mtt.markdownSequence[0])
.arg(tags.first)
.arg(tags.second);
}
void TestTextFormatter::doubleSignWorkCasesShowSymbols()
{
workCasesTest(markdownFunction,
DOUBLE_SIGN_MARKDOWN,
DOUBLE_SIGN_WORK_CASES,
true,
doubleSignWorkCasesProcessInput,
doubleSignWorkCasesProcessOutput);
}
void TestTextFormatter::doubleSignWorkCasesHideSymbols()
{
workCasesTest(markdownFunction,
DOUBLE_SIGN_MARKDOWN,
DOUBLE_SIGN_WORK_CASES,
false,
doubleSignWorkCasesProcessInput,
doubleSignWorkCasesProcessOutput);
}
static QString multiSignWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt)
{
return str.arg(mtt.markdownSequence);
}
static QString multiSignWorkCasesProcessOutput(const QString& str,
const MarkdownToTags& mtt,
bool showSymbols)
{
const auto tags = mtt.htmlTags;
return str.arg(showSymbols ? mtt.markdownSequence : "", tags.first, tags.second);
}
void TestTextFormatter::multiSignWorkCasesHideSymbols()
{
workCasesTest(markdownFunction,
MULTI_SIGN_MARKDOWN,
MULTI_SIGN_WORK_CASES,
false,
multiSignWorkCasesProcessInput,
multiSignWorkCasesProcessOutput);
}
void TestTextFormatter::multiSignWorkCasesShowSymbols()
{
workCasesTest(markdownFunction,
MULTI_SIGN_MARKDOWN,
MULTI_SIGN_WORK_CASES,
true,
multiSignWorkCasesProcessInput,
multiSignWorkCasesProcessOutput);
}
void TestTextFormatter::commonExceptionsShowSymbols()
{
exceptionsTest(markdownFunction, ALL_MARKDOWN_TYPES, COMMON_EXCEPTIONS, true);
}
void TestTextFormatter::commonExceptionsHideSymbols()
{
exceptionsTest(markdownFunction, ALL_MARKDOWN_TYPES, COMMON_EXCEPTIONS, false);
}
void TestTextFormatter::singleSignExceptionsShowSymbols()
{
exceptionsTest(markdownFunction, SINGLE_SIGN_MARKDOWN, SINGLE_SIGN_EXCEPTIONS, true);
}
void TestTextFormatter::singleSignExceptionsHideSymbols()
{
exceptionsTest(markdownFunction, SINGLE_SIGN_MARKDOWN, SINGLE_SIGN_EXCEPTIONS, false);
}
void TestTextFormatter::singleAndDoubleMarkdownExceptionsShowSymbols()
{
exceptionsTest(markdownFunction,
SINGLE_AND_DOUBLE_MARKDOWN,
SINGLE_AND_DOUBLE_SIGN_EXCEPTIONS,
true);
}
void TestTextFormatter::singleAndDoubleMarkdownExceptionsHideSymbols()
{
exceptionsTest(markdownFunction,
SINGLE_AND_DOUBLE_MARKDOWN,
SINGLE_AND_DOUBLE_SIGN_EXCEPTIONS,
false);
}
void TestTextFormatter::mixedFormattingSpecialCases()
{
specialCasesTest(markdownFunction, MIXED_FORMATTING_SPECIAL_CASES);
}
void TestTextFormatter::urlTest()
{
urlHighlightTest(urlHighlightFunction, URL_CASES);
}
QTEST_GUILESS_MAIN(TestTextFormatter)
#include "textformatter_test.moc"