diff --git a/test/chatlog/textformatter_test.cpp b/test/chatlog/textformatter_test.cpp index 5a71e0a60..49b4eac16 100644 --- a/test/chatlog/textformatter_test.cpp +++ b/test/chatlog/textformatter_test.cpp @@ -20,59 +20,138 @@ #include "src/chatlog/textformatter.h" #include -#include -#include #include -#include -#include #include +#include -using StringToString = QMap; +#define PAIR_FORMAT(input, output) {QStringLiteral(input), QStringLiteral(output)} -static const StringToString signsToTags{{"*", "b"}, {"**", "b"}, {"/", "i"}}; +using StringPair = QPair; -static const StringToString - commonWorkCases{// Basic - {QStringLiteral("%1a%1"), QStringLiteral("<%2>%1a%1")}, - {QStringLiteral("%1aa%1"), QStringLiteral("<%2>%1aa%1")}, - {QStringLiteral("%1aaa%1"), QStringLiteral("<%2>%1aaa%1")}, +static const StringPair TAGS[] { + PAIR_FORMAT("", ""), + PAIR_FORMAT("", ""), + PAIR_FORMAT("", ""), + PAIR_FORMAT("", ""), + PAIR_FORMAT("", ""), +}; - // Additional text from both sides - {QStringLiteral("aaa%1a%1"), QStringLiteral("aaa<%2>%1a%1")}, - {QStringLiteral("%1a%1aaa"), QStringLiteral("<%2>%1a%1aaa")}, +enum StyleType { + BOLD, + ITALIC, + UNDERLINE, + STRIKE, + CODE, +}; - // Must allow same formatting more than one time, divided by two and more - // symbols due to QRegularExpressionIterator - {QStringLiteral("%1aaa%1 aaa %1aaa%1"), - QStringLiteral("<%2>%1aaa%1 aaa <%2>%1aaa%1")}}; +/** + * @brief The MarkdownToTags struct maps sequence of markdown symbols to HTML tags according this + * sequence + */ +struct MarkdownToTags +{ + QString markdownSequence; + StringPair htmlTags; +}; -static const QVector - commonExceptions{// No whitespaces near to formatting symbols from both sides - QStringLiteral("%1 a%1"), QStringLiteral("%1a %1"), +static const QVector SINGLE_SIGN_MARKDOWN { + {QStringLiteral("*"), TAGS[StyleType::BOLD]}, + {QStringLiteral("/"), TAGS[StyleType::ITALIC]}, + {QStringLiteral("_"), TAGS[StyleType::UNDERLINE]}, + {QStringLiteral("~"), TAGS[StyleType::STRIKE]}, + {QStringLiteral("`"), TAGS[StyleType::CODE]}, +}; - // No newlines - QStringLiteral("%1aa\n%1"), +static const QVector DOUBLE_SIGN_MARKDOWN { + {QStringLiteral("**"), TAGS[StyleType::BOLD]}, + {QStringLiteral("//"), TAGS[StyleType::ITALIC]}, + {QStringLiteral("__"), TAGS[StyleType::UNDERLINE]}, + {QStringLiteral("~~"), TAGS[StyleType::STRIKE]}, +}; - // Only exact combinations of symbols must encapsulate formatting string - QStringLiteral("%1%1aaa%1"), QStringLiteral("%1aaa%1%1")}; +static const QVector MULTI_SIGN_MARKDOWN { + {QStringLiteral("```"), TAGS[StyleType::CODE]}, +}; -static const StringToString singleSlash{ - // Must work with inserted tags - {QStringLiteral("/aaaaaa aaa/"), QStringLiteral("aaaaaa aaa")}}; +/** + * @brief Creates single container from two + */ +template +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 StringToString doubleSign{ - {QStringLiteral("**aaa * aaa**"), QStringLiteral("aaa * aaa")}}; +static const QVector ALL_MARKDOWN_TYPES = concat(concat(SINGLE_SIGN_MARKDOWN, + DOUBLE_SIGN_MARKDOWN), + MULTI_SIGN_MARKDOWN); -static const StringToString mixedFormatting{ +static const QVector 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 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 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 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"), +}; + +// any type of markdown must fail for this data +static const QVector COMMON_EXCEPTIONS { + // No empty formatting string + QStringLiteral("%1%1"), + // Formatting text must not start/end with whitespace symbols + QStringLiteral("%1 %1"), QStringLiteral("%1 a%1"), QStringLiteral("%1a %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 SINGLE_AND_DOUBLE_SIGN_EXCEPTIONS { + // 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 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 MIXED_FORMATTING_SPECIAL_CASES { // Must allow mixed formatting if there is no tag overlap in result - {QStringLiteral("aaa *aaa /aaa/ aaa*"), QStringLiteral("aaa aaa aaa aaa")}, - {QStringLiteral("aaa *aaa /aaa* aaa/"), QStringLiteral("aaa aaa /aaa aaa/")}}; - -static const StringToString multilineCode{ - // Must allow newlines - {QStringLiteral("```int main()\n{\n return 0;\n}```"), - QStringLiteral("int main()\n{\n return 0;\n}")}}; + PAIR_FORMAT("aaa *aaa /aaa/ aaa*", "aaa aaa aaa aaa"), + PAIR_FORMAT("aaa *aaa /aaa* aaa/", "aaa *aaa aaa* aaa"), +}; static const StringToString urlCases{ {QStringLiteral("https://github.com/qTox/qTox/issues/4233"), @@ -116,6 +195,87 @@ static const StringToString urlCases{ "www.site.com/part1/part2")}, }; +#undef PAIR_FORMAT + +using MarkdownFunction = QString (*)(const QString&, bool); +using InputProcessor = std::function; +using OutputProcessor = std::function; + +/** + * @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, + const QVector& 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; + qDebug() << "Input:" << input; + QString output = processOutput != nullptr ? processOutput(data.second, mtt, showSymbols) + : data.second; + qDebug() << "Expected output:" << output; + QString result = applyMarkdown(input, showSymbols); + qDebug() << "Observed output:" << result; + 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, + const QVector& exceptions, + bool showSymbols) +{ + for (const MarkdownToTags& mtt: markdownToTags) { + for (const QString& e: exceptions) { + QString processedException = e.arg(mtt.markdownSequence); + qDebug() << "Exception: " << processedException; + 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& pairs) +{ + for (const auto& p : pairs) { + qDebug() << "Input:" << p.first; + qDebug() << "Expected output:" << p.second; + QString result = applyMarkdown(p.first, false); + qDebug() << "Observed output:" << result; + QVERIFY(p.second == result); + } +} + /** * @brief Testing cases which are common for all types of formatting except multiline code * @param noSymbols True if it's not allowed to show formatting symbols @@ -158,85 +318,168 @@ class TestTextFormatter : public QObject { Q_OBJECT private slots: - void singleSignNoSymbolsTest(); - void slashNoSymbolsTest(); - void doubleSignNoSymbolsTest(); - void singleSignWithSymbolsTest(); - void slashWithSymbolsTest(); - void doubleSignWithSymbolsTest(); - void singleSignExceptionsTest(); - void slashExceptionsTest(); - void doubleSignExceptionsTest(); - void slashSpecialTest(); - void doubleSignSpecialTest(); - void mixedFormattingTest(); - void multilineCodeTest(); + void commonWorkCasesShowSymbols(); + void commonWorkCasesHideSymbols(); + void singleSignWorkCasesShowSymbols(); + void singleSignWorkCasesHideSymbols(); + void doubleSignWorkCasesShowSymbols(); + void doubleSignWorkCasesHideSymbols(); + void commonExceptionsShowSymbols(); + void commonExceptionsHideSymbols(); + void singleSignExceptionsShowSymbols(); + void singleSignExceptionsHideSymbols(); + void singleAndDoubleMarkdownExceptionsShowSymbols(); + void singleAndDoubleMarkdownExceptionsHideSymbols(); + void mixedFormattingSpecialCases(); void urlTest(); +private: + MarkdownFunction markdownFunction; }; -void TestTextFormatter::singleSignNoSymbolsTest() +static QString commonWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt) { - commonTest(false, commonWorkCases, "*"); + return str.arg(mtt.markdownSequence); } -void TestTextFormatter::slashNoSymbolsTest() +static QString commonWorkCasesProcessOutput(const QString& str, + const MarkdownToTags& mtt, + bool showSymbols) { - commonTest(false, commonWorkCases, "/"); + const StringPair& tags = mtt.htmlTags; + return str.arg(showSymbols ? mtt.markdownSequence : QString{}).arg(tags.first).arg(tags.second); } -void TestTextFormatter::doubleSignNoSymbolsTest() +void TestTextFormatter::commonWorkCasesShowSymbols() { - commonTest(false, commonWorkCases, "**"); + workCasesTest(markdownFunction, + ALL_MARKDOWN_TYPES, + COMMON_WORK_CASES, + true, + commonWorkCasesProcessInput, + commonWorkCasesProcessOutput); } -void TestTextFormatter::singleSignWithSymbolsTest() +void TestTextFormatter::commonWorkCasesHideSymbols() { - commonTest(true, commonWorkCases, "*"); + workCasesTest(markdownFunction, + ALL_MARKDOWN_TYPES, + COMMON_WORK_CASES, + false, + commonWorkCasesProcessInput, + commonWorkCasesProcessOutput); } -void TestTextFormatter::slashWithSymbolsTest() +static QString singleSignWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt) { - commonTest(true, commonWorkCases, "/"); + return str.arg(mtt.markdownSequence); } -void TestTextFormatter::doubleSignWithSymbolsTest() +static QString singleSignWorkCasesProcessOutput(const QString& str, + const MarkdownToTags& mtt, + bool showSymbols) { - commonTest(true, commonWorkCases, "**"); + const StringPair& tags = mtt.htmlTags; + return str.arg(showSymbols ? mtt.markdownSequence : "") + .arg(tags.first) + .arg(tags.second) + .arg(mtt.markdownSequence); } -void TestTextFormatter::singleSignExceptionsTest() +void TestTextFormatter::singleSignWorkCasesShowSymbols() { - commonExceptionsTest("*"); + workCasesTest(markdownFunction, + SINGLE_SIGN_MARKDOWN, + SINGLE_SIGN_WORK_CASES, + true, + singleSignWorkCasesProcessInput, + singleSignWorkCasesProcessOutput); } -void TestTextFormatter::slashExceptionsTest() +void TestTextFormatter::singleSignWorkCasesHideSymbols() { - commonExceptionsTest("/"); + workCasesTest(markdownFunction, + SINGLE_SIGN_MARKDOWN, + SINGLE_SIGN_WORK_CASES, + false, + singleSignWorkCasesProcessInput, + singleSignWorkCasesProcessOutput); } -void TestTextFormatter::doubleSignExceptionsTest() +static QString doubleSignWorkCasesProcessInput(const QString& str, const MarkdownToTags& mtt) { - commonExceptionsTest("**"); + return str.arg(mtt.markdownSequence).arg(mtt.markdownSequence[0]); } -void TestTextFormatter::slashSpecialTest() +static QString doubleSignWorkCasesProcessOutput(const QString& str, + const MarkdownToTags& mtt, + bool showSymbols) { - specialTest(singleSlash); + const StringPair& tags = mtt.htmlTags; + return str.arg(showSymbols ? mtt.markdownSequence : "") + .arg(mtt.markdownSequence[0]) + .arg(tags.first) + .arg(tags.second); } -void TestTextFormatter::doubleSignSpecialTest() +void TestTextFormatter::doubleSignWorkCasesShowSymbols() { - specialTest(doubleSign); + workCasesTest(markdownFunction, + DOUBLE_SIGN_MARKDOWN, + DOUBLE_SIGN_WORK_CASES, + true, + doubleSignWorkCasesProcessInput, + doubleSignWorkCasesProcessOutput); } -void TestTextFormatter::mixedFormattingTest() +void TestTextFormatter::doubleSignWorkCasesHideSymbols() { - specialTest(mixedFormatting); + workCasesTest(markdownFunction, + DOUBLE_SIGN_MARKDOWN, + DOUBLE_SIGN_WORK_CASES, + false, + doubleSignWorkCasesProcessInput, + doubleSignWorkCasesProcessOutput); } -void TestTextFormatter::multilineCodeTest() +void TestTextFormatter::commonExceptionsShowSymbols() { - specialTest(multilineCode); + 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()