8#include "katedocument.h"
9#include "katepartdebug.h"
11#include "scripttester_p.h"
19#include <QLatin1StringView>
21#include <QStandardPaths>
22#include <QVarLengthArray>
27using namespace Qt::Literals::StringLiterals;
35constexpr QStringView
operator""_sv(
const char16_t *str,
size_t size)
noexcept
37 return QStringView(str, size);
49static QString getPath(QJSEngine *engine,
const QString &name,
const QStringList &dirs, QString *error)
51 for (
const QString &dir : dirs) {
58 *
error = u
"file '%1' not found in %2"_sv.arg(name, dirs.join(u
", "_sv));
72static QString getModulePath(QJSEngine *engine,
const QString &fileName,
const QStringList &dirs, QString *error)
75 if (QFileInfo(fileName).isRelative()) {
76 for (
const QString &dir : dirs) {
77 QString
path =
dir % u
'/' % fileName;
90 *
error = u
"file '%1' not found in working directory"_sv.arg(fileName);
92 *
error = u
"file '%1' not found in %2 and working directory"_sv.arg(fileName, dirs.join(u
", "_sv));
107static bool readFile(QJSEngine *engine,
const QString &sourceUrl, QString *content, QString *error)
109 QFile file(sourceUrl);
111 QTextStream stream(&file);
112 *content = stream.readAll();
116 *
error = u
"reading error for '%1': %2"_sv.arg(sourceUrl, file.errorString());
124static void writeCarretLine(QTextStream &stream,
const ScriptTester::Colors &colors,
int column)
130 stream << colors.carret;
132 stream << colors.
reset;
139static void writeLabel(QTextStream &stream,
const ScriptTester::Colors &colors,
bool colored, QLatin1StringView text)
142 stream << colors.labelInfo << text << colors.reset;
152static void readString(
const QJSValue &obj,
const QString &name, SetFn &&setFn)
155 if (!value.isUndefined()) {
156 setFn(value.toString());
164static void readInt(
const QJSValue &obj,
const QString &name, SetFn &&setFn)
167 if (!value.isUndefined()) {
168 setFn(value.toInt());
176static void readBool(
const QJSValue &obj,
const QString &name, SetFn &&setFn)
179 if (!value.isUndefined()) {
180 setFn(value.toBool());
187static qsizetype computeOffsetDifference(QStringView a, QStringView b)
189 qsizetype n = qMin(a.
size(), b.
size());
193 if (a[i].isLowSurrogate() && b[i].isLowSurrogate()) {
194 return qMax(0, i - 1);
206 qsizetype size()
const
216static VirtualText findVirtualText(QStringView str, qsizetype pos, QChar c)
218 VirtualText res{str.
indexOf(c, pos), -1};
219 if (res.pos1 != -1) {
220 res.pos2 = res.pos1 + 1;
221 while (res.pos2 < str.
size() && str[res.pos2] == c) {
231static qsizetype filePrefixLen(QStringView str)
247static QStringView skipFilePrefix(QStringView str)
249 return str.
sliced(filePrefixLen(str));
255static inline QJSValue getStack(
const QJSValue &exception)
257 return exception.
property(u
"stack"_s);
263static inline QJSValue generateStack(QJSEngine *engine)
270 QStringView funcName;
271 QStringView filePrefixOrMessage;
272 QStringView fileName;
273 QStringView lineNumber;
274 QStringView remaining;
281static StackLine parseStackLine(QStringView stack)
288 qsizetype pos = stack.
indexOf(
'@'_L1);
290 ret.funcName = stack.
first(pos);
291 stack = stack.
sliced(pos + 1);
295 pos = filePrefixLen(stack);
296 ret.filePrefixOrMessage = stack.
first(pos);
298 auto endLine = stack.
indexOf(
'\n'_L1, pos);
299 auto line = stack.
sliced(pos, ((endLine < 0) ? stack.
size() : endLine) - pos);
303 ret.fileName = line.sliced(0, i);
304 ret.lineNumber = line.sliced(i + 1);
306 ret.filePrefixOrMessage = line;
310 ret.remaining = stack.
sliced(endLine + 1);
323static void pushException(QString &buffer, ScriptTester::Colors &colors, QStringView stack, QStringView prefix)
326 if (!stack.
isEmpty() && stack[0] == u
'%') {
327 auto pos = stack.
indexOf(
'\n'_L1);
329 buffer += colors.error % prefix % stack % colors.reset % u
'\n';
332 stack = stack.
sliced(pos + 1);
337 auto stackLine = parseStackLine(stack);
339 buffer += colors.error % prefix % colors.reset
340 % colors.program % stackLine.funcName % colors.reset
341 % colors.error % u
'@' % stackLine.filePrefixOrMessage % colors.reset
342 % colors.fileName % stackLine.fileName % colors.reset
343 % colors.error % u
':' % colors.reset
344 % colors.lineNumber % stackLine.lineNumber % colors.reset
347 stack = stackLine.remaining;
351static inline bool cursorSameAsSecondary(
const ScriptTester::Placeholders &placeholders)
353 return placeholders.cursor == placeholders.secondaryCursor;
356static inline bool selectionStartSameAsSecondary(
const ScriptTester::Placeholders &placeholders)
358 return placeholders.selectionStart == placeholders.selectionStart;
361static inline bool selectionEndSameAsSecondary(
const ScriptTester::Placeholders &placeholders)
363 return placeholders.selectionEnd == placeholders.selectionEnd;
366static inline bool selectionSameAsSecondary(
const ScriptTester::Placeholders &placeholders)
368 return selectionStartSameAsSecondary(placeholders) || selectionEndSameAsSecondary(placeholders);
373ScriptTester::EditorConfig ScriptTester::makeEditorConfig()
377 .indentationMode = u
"none"_s,
378 .indentationWidth = 4,
380 .replaceTabs =
false,
381 .autoBrackets =
false,
391struct ScriptTester::TextItem {
441 SecondarySelectionEnd,
442 VirtualBlockSelectionEnd,
446 EmptySecondarySelectionStart,
453 EmptySecondarySelectionEnd,
456 SecondarySelectionStart,
457 VirtualBlockSelectionStart,
472 StartCharacterElement = NewLine,
477 int virtualTextLen = 0;
479 bool isCharacter()
const
481 return kind >= StartCharacterElement;
484 bool isCursor()
const
486 return kind == Cursor || kind == SecondaryCursor;
489 bool isSelectionStart()
const
491 return kind == SelectionStart || kind == SecondarySelectionStart;
494 bool isSelectionEnd()
const
496 return kind == SelectionEnd || kind == SecondarySelectionEnd;
499 bool isSelection(
bool hasVirtualBlockSelection)
const
503 case SecondarySelectionEnd:
505 case SecondarySelectionStart:
507 case VirtualBlockSelectionEnd:
508 case BlockSelectionEnd:
509 case VirtualBlockSelectionStart:
510 case BlockSelectionStart:
511 return hasVirtualBlockSelection;
517 bool isBlockSelectionOrVirtual()
const
520 case VirtualBlockSelectionEnd:
521 case BlockSelectionEnd:
522 case VirtualBlockCursor:
523 case VirtualBlockSelectionStart:
524 case BlockSelectionStart:
531 bool isEmptySelection()
const
534 case EmptySelectionEnd:
535 case EmptySecondarySelectionEnd:
536 case EmptySelectionStart:
537 case EmptySecondarySelectionStart:
545ScriptTester::DocumentText::DocumentText() =
default;
546ScriptTester::DocumentText::~DocumentText() =
default;
555std::size_t ScriptTester::DocumentText::addItems(QStringView str,
int kind, QChar c)
557 const auto n = items.size();
560 while (-1 != (pos = str.
indexOf(c, pos))) {
561 items.push_back({pos, TextItem::Kind(kind)});
565 return items.size() - n;
579std::size_t ScriptTester::DocumentText::addSelectionItems(QStringView str,
int kind, QChar
start, QChar end)
581 const auto n = items.size();
585 qsizetype pos2 = str.
indexOf(end, pos + 1);
590 constexpr int offsetEnd = TextItem::SelectionEnd - TextItem::SelectionStart;
591 static_assert(TextItem::SecondarySelectionStart + offsetEnd == TextItem::SecondarySelectionEnd);
593 constexpr int offsetEmptyStart = TextItem::EmptySelectionStart - TextItem::SelectionStart;
594 static_assert(TextItem::SecondarySelectionStart + offsetEmptyStart == TextItem::EmptySecondarySelectionStart);
596 constexpr int offsetEmptyEnd = TextItem::EmptySelectionEnd - TextItem::SelectionStart;
597 static_assert(TextItem::SecondarySelectionStart + offsetEmptyEnd == TextItem::EmptySecondarySelectionEnd);
599 int offset1 = (pos + 1 == pos2) ? offsetEmptyStart : 0;
600 int offset2 = (pos + 1 == pos2) ? offsetEmptyEnd : offsetEnd;
601 items.push_back({pos, TextItem::Kind(kind + offset1)});
602 items.push_back({pos2, TextItem::Kind(kind + offset2)});
607 return (items.size() - n) / 2;
613void ScriptTester::DocumentText::computeBlockSelectionItems()
625 if (selection.start().line() == -1 || selection.numberOfLines() <= (selection.columnWidth() ? 0 : 1)) {
629 const auto nbLine = selection.numberOfLines();
630 const auto startCursor = selection.start();
631 const auto endCursor = selection.end();
633 const auto nbItem = items.size();
638 if (startCursor.column() != endCursor.column()) {
639 items.resize(nbItem + nbLine * 2 + 1);
644 items[nbItem] = {text.
size(), TextItem::NewLine, 0};
646 items.resize(nbItem + nbLine - 1);
649 using Iterator = std::vector<TextItem>::iterator;
651 Iterator itemIt = items.begin();
652 Iterator itemEnd = itemIt + nbItem;
654 Iterator outIt = itemEnd + (startCursor.column() != endCursor.column());
656 auto advanceUntilNewLine = [](Iterator &itemIt) {
657 while (itemIt->kind != TextItem::NewLine) {
663 qsizetype textPos = 0;
668 if (startCursor.line() > 0) {
670 advanceUntilNewLine(itemIt);
671 if (++line == startCursor.line()) {
672 textPos = itemIt->pos + 1;
683 auto advanceAndPushItem = [&outIt, &textPos](Iterator &itemIt,
int column, TextItem::Kind kind) {
684 while (!itemIt->virtualTextLen && itemIt->pos - textPos < column && itemIt->kind != TextItem::NewLine) {
690 if (itemIt->pos - textPos >= column) {
691 *outIt = {textPos + column, kind};
693 vlen = column - (itemIt->pos - textPos);
694 *outIt = {itemIt->pos, kind, vlen};
705 if (startCursor.column() != endCursor.column()) {
706 vlen = advanceAndPushItem(itemIt, endCursor.column(), TextItem::BlockSelectionStart);
708 advanceUntilNewLine(itemIt);
709 itemIt->virtualTextLen = qMax(itemIt->virtualTextLen, vlen);
710 textPos = itemIt->pos + 1;
713 int leftColumn = startCursor.column();
714 int rightColumn = endCursor.column();
715 if (startCursor.column() > endCursor.column()) {
716 std::swap(leftColumn, rightColumn);
722 while (++line < endCursor.line()) {
723 if (leftColumn != rightColumn) {
724 advanceAndPushItem(itemIt, leftColumn, TextItem::VirtualBlockSelectionStart);
727 int vlen = advanceAndPushItem(itemIt, rightColumn, (leftColumn != rightColumn) ? TextItem::VirtualBlockSelectionEnd : TextItem::VirtualBlockCursor);
728 advanceUntilNewLine(itemIt);
729 itemIt->virtualTextLen = qMax(itemIt->virtualTextLen, vlen);
730 textPos = itemIt->pos + 1;
737 if (startCursor.column() != endCursor.column()) {
738 int vlen = advanceAndPushItem(itemIt, startCursor.column(), TextItem::BlockSelectionEnd);
740 advanceUntilNewLine(itemIt);
741 itemIt->virtualTextLen = qMax(itemIt->virtualTextLen, vlen);
747 items[nbItem] = items.back();
755void ScriptTester::DocumentText::insertFormattingItems(DocumentTextFormat format)
757 const auto nbItem = items.size();
759 if (!hasFormattingItems) {
760 hasFormattingItems =
true;
766 case DocumentTextFormat::Raw:
768 case DocumentTextFormat::EscapeForDoubleQuote:
769 addItems(text, TextItem::Backslash, u
'\\');
770 addItems(text, TextItem::DoubleQuote, u
'"');
772 case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral:
773 case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder:
774 case DocumentTextFormat::ReplaceTabWithPlaceholder:
775 addItems(text, TextItem::Tab, u
'\t');
780 if (blockSelection && !hasBlockSelectionItems) {
781 hasBlockSelectionItems =
true;
782 computeBlockSelectionItems();
785 if (nbItem != items.size()) {
793void ScriptTester::DocumentText::sortItems()
795 auto cmp = [](
const TextItem &a,
const TextItem &b) {
802 if (a.virtualTextLen < b.virtualTextLen) {
805 if (a.virtualTextLen > b.virtualTextLen) {
808 return a.kind < b.kind;
810 std::sort(items.begin(), items.end(), cmp);
819QString ScriptTester::DocumentText::setText(QStringView input,
const Placeholders &placeholders)
823 secondaryCursors.clear();
824 secondaryCursorsWithSelection.clear();
825 hasFormattingItems =
false;
826 hasBlockSelectionItems =
false;
830 totalLine = 1 + addItems(input, TextItem::NewLine, u
'\n');
832#define RETURN_IF_VIRTUAL_TEXT_CONFLICT(hasItem, placeholderName) \
833 if (hasItem && placeholders.hasVirtualText() && placeholders.virtualText == placeholders.placeholderName) { \
834 return u"virtualText placeholder conflicts with " #placeholderName ""_s; \
842 if (placeholders.hasSecondaryCursor()) {
843 totalCursor = addItems(input, TextItem::SecondaryCursor, placeholders.secondaryCursor);
844 RETURN_IF_VIRTUAL_TEXT_CONFLICT(totalCursor, secondaryCursor);
848 if (totalCursor && (!placeholders.hasCursor() || cursorSameAsSecondary(placeholders))) {
849 items[items.size() - totalCursor].kind = TextItem::Cursor;
854 if (placeholders.hasCursor() && (!placeholders.hasSecondaryCursor() || !cursorSameAsSecondary(placeholders))) {
855 const auto nbCursor = addItems(input, TextItem::Cursor, placeholders.cursor);
857 return u
"primary cursor set multiple times"_s;
859 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbCursor, cursor);
860 totalCursor += nbCursor;
868 if (placeholders.hasSecondarySelection()) {
869 totalSelection = addSelectionItems(input, TextItem::SecondarySelectionStart, placeholders.secondarySelectionStart, placeholders.secondarySelectionEnd);
870 RETURN_IF_VIRTUAL_TEXT_CONFLICT(totalSelection, secondarySelectionStart);
871 RETURN_IF_VIRTUAL_TEXT_CONFLICT(totalSelection, secondarySelectionEnd);
875 if (totalSelection && (!placeholders.hasSelection() || selectionSameAsSecondary(placeholders))) {
876 if (placeholders.hasSelection() && (!selectionStartSameAsSecondary(placeholders) || !selectionEndSameAsSecondary(placeholders))) {
877 return u
"primary selection placeholder conflicts with secondary selection placeholder"_s;
879 auto &kind1 = items[items.size() - totalSelection * 2 + 0].kind;
880 auto &kind2 = items[items.size() - totalSelection * 2 + 1].kind;
881 bool isEmptySelection = kind1 == TextItem::EmptySecondarySelectionStart;
882 kind1 = isEmptySelection ? TextItem::EmptySelectionStart : TextItem::SelectionStart;
883 kind2 = isEmptySelection ? TextItem::EmptySelectionEnd : TextItem::SelectionEnd;
888 if (placeholders.hasSelection() && (!placeholders.hasSecondarySelection() || !selectionSameAsSecondary(placeholders))) {
889 const auto nbSelection = addSelectionItems(input, TextItem::SelectionStart, placeholders.selectionStart, placeholders.selectionEnd);
890 if (nbSelection > 1) {
891 return u
"primary selection set multiple times"_s;
893 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbSelection, selectionStart);
894 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbSelection, selectionEnd);
895 totalSelection += nbSelection;
898#undef RETURN_IF_VIRTUAL_TEXT_CONFLICT
904 VirtualText virtualText{-1, -1};
906 if (placeholders.hasVirtualText()) {
907 virtualText = findVirtualText(input, 0, placeholders.virtualText);
910 if (virtualText.pos2 != -1 && (totalCursor > 1 || totalSelection > 1)) {
911 return u
"virtualText is incompatible with multi-cursor/selection"_s;
922 int charConsumedInPreviousLines = 0;
927 const TextItem *selectionEndItem =
nullptr;
928 qsizetype ignoredChars = 0;
929 qsizetype virtualTextLen = 0;
930 qsizetype lastPos = -1;
941 for (
auto &item : items) {
945 if (lastPos == item.pos) {
955 if (virtualText.pos2 != -1 && virtualText.pos2 <= item.pos) {
957 if (item.kind == TextItem::NewLine || virtualText.pos2 != item.pos) {
960 const auto pos = text.
size() + ignoredChars;
961 text.append(input.
sliced(pos, item.pos - pos - virtualText.size()));
963 ignoredChars += virtualText.size();
964 virtualTextLen += virtualText.size();
965 virtualText = findVirtualText(input, virtualText.pos2, placeholders.virtualText);
966 }
else if (virtualTextLen) {
968 if (item.pos != text.
size() + ignoredChars) {
977 item.pos -= ignoredChars;
978 item.virtualTextLen = virtualTextLen;
980 auto cursorFromCurrentItem = [&] {
981 return Cursor(line, item.pos - charConsumedInPreviousLines + item.virtualTextLen);
985 case TextItem::Cursor:
986 cursor = cursorFromCurrentItem();
988 case TextItem::SelectionStart:
989 case TextItem::EmptySelectionStart:
990 selectionStart = cursorFromCurrentItem();
992 case TextItem::SelectionEnd:
993 case TextItem::EmptySelectionEnd:
994 selectionEndItem = &item;
995 selectionEnd = cursorFromCurrentItem();
997 case TextItem::SecondaryCursor:
998 secondaryCursors.push_back({cursorFromCurrentItem(),
Range::invalid()});
1000 case TextItem::SecondarySelectionStart:
1001 case TextItem::EmptySecondarySelectionStart:
1002 secondarySelectionStart = cursorFromCurrentItem();
1004 case TextItem::SecondarySelectionEnd:
1005 case TextItem::EmptySecondarySelectionEnd:
1006 secondaryCursorsWithSelection.push_back({
Cursor::invalid(), {secondarySelectionStart, cursorFromCurrentItem()}});
1010 charConsumedInPreviousLines = item.pos + 1;
1016 const auto pos = text.
size() + ignoredChars;
1017 const auto len = item.pos + ignoredChars - pos;
1018 text.append(input.
sliced(pos, len));
1023 if ((virtualText.pos2 != -1 && virtualText.pos2 != text.
size()) || (virtualTextLen && text.
size() + ignoredChars != input.
size())) {
1024 const auto pos = (virtualText.pos1 != -1) ? virtualText.pos1 : text.size() + ignoredChars - virtualTextLen;
1025 return u
"virtual text found at position %1, but not followed by a cursor or selection then a line break or end of text"_s.arg(pos);
1028 if (!secondaryCursorsWithSelection.isEmpty() && selectionStart.line() == -1) {
1029 return u
"secondary selections are added without any primary selection"_s;
1032 if (!secondaryCursors.empty() && cursor.line() == -1) {
1033 return u
"secondary cursors are added without any primary cursor"_s;
1036 text += input.
sliced(text.
size() + ignoredChars);
1051 int countSelection = 0;
1052 qsizetype lastCursorPos = -1;
1053 qsizetype lastSelectionPos = -1;
1054 for (
auto &item : items) {
1055 if (item.isSelectionStart()) {
1057 if ((countSelection & 1) && lastSelectionPos != item.pos) {
1058 lastSelectionPos = item.pos;
1061 }
else if (item.isSelectionEnd()) {
1063 if (!(countSelection & 1) && lastSelectionPos != item.pos) {
1064 lastSelectionPos = item.pos;
1067 }
else if (item.isCursor()) {
1068 if (countSelection & 1) {
1069 return u
"cursor inside a selection"_s;
1071 if (lastCursorPos == item.pos) {
1072 return u
"one or more cursors overlap"_s;
1074 lastCursorPos = item.pos;
1076 }
else if (item.isEmptySelection()) {
1077 if (!(countSelection & 1)) {
1083 return u
"selection %1 is overlapped"_s.arg(countSelection / 2 + 1);
1097 if (!secondaryCursors.empty() && !secondaryCursorsWithSelection.isEmpty()) {
1098 auto it = secondaryCursors.begin();
1099 auto end = secondaryCursors.end();
1100 auto it2 = secondaryCursorsWithSelection.begin();
1101 auto end2 = secondaryCursorsWithSelection.end();
1104 while (it != end && it2 != end2) {
1105 if (it2->range.end() < it->pos) {
1106 it2->pos = it2->range.end();
1108 }
else if (it2->range.start() == it->pos || it2->range.end() == it->pos) {
1111 it->pos.setLine(-1);
1119 for (; it2 != end2; ++it2) {
1120 it2->pos = it2->range.end();
1124 const auto n = secondaryCursorsWithSelection.size();
1125 for (
auto &c : secondaryCursors) {
1126 if (c.pos.line() != -1) {
1127 secondaryCursorsWithSelection.append(c);
1130 if (n != secondaryCursorsWithSelection.size()) {
1131 std::sort(secondaryCursorsWithSelection.begin(), secondaryCursorsWithSelection.end());
1133 }
else if (!secondaryCursorsWithSelection.isEmpty()) {
1134 for (
auto &c : secondaryCursorsWithSelection) {
1135 c.pos = c.range.end();
1138 secondaryCursorsWithSelection.assign(secondaryCursors.begin(), secondaryCursors.end());
1144 if (cursor.line() == -1) {
1145 if (selectionEndItem) {
1147 auto afterSelection = items.begin() + (selectionEndItem - items.data()) + 1;
1148 items.insert(afterSelection, {selectionEndItem->pos, TextItem::Cursor, selectionEndItem->virtualTextLen});
1149 cursor = selectionEnd;
1152 const auto virtualTextLen = items.empty() ? 0 : items.back().virtualTextLen;
1153 items.push_back({input.
size(), TextItem::Cursor, virtualTextLen});
1154 cursor =
Cursor(line, input.
size() - charConsumedInPreviousLines);
1158 selection = {selectionStart, selectionEnd};
1161 if (selection.start().line() != -1 && !selection.boundaryAtCursor(cursor)) {
1162 return u
"the cursor is not at the limit of the selection"_s;
1168ScriptTester::ScriptTester(QIODevice *output,
1169 const Format &format,
1171 const TestExecutionConfig &executionConfig,
1172 const DiffCommand &diffCmd,
1173 Placeholders placeholders,
1182 , m_fallbackPlaceholders(format.fallbackPlaceholders)
1183 , m_defaultPlaceholders(placeholders)
1184 , m_placeholders(placeholders)
1185 , m_editorConfig(makeEditorConfig())
1189 , m_executionConfig(executionConfig)
1190 , m_diffCmd(diffCmd)
1193 auto *docConfig = m_doc->config();
1194 docConfig->configStart();
1195 docConfig->setIndentPastedText(
true);
1204 QString fullName = getPath(m_engine, name, m_paths.scripts, &contentOrError);
1206 readFile(m_engine, fullName, &contentOrError, &contentOrError);
1208 return contentOrError;
1211void ScriptTester::require(
const QString &name)
1214 auto it = m_libraryFiles.find(name);
1215 if (it != m_libraryFiles.end()) {
1217 if (!it->isEmpty()) {
1231 if (!
readFile(m_engine, fullName, &program, &*it)) {
1236 const QJSValue val = m_engine->evaluate(program, fullName);
1243 m_engine->throwError(val);
1246void ScriptTester::debug(
const QString &message)
1248 const auto requireStack = m_format.debugOptions.testAnyFlags(DebugOption::WriteStackTrace | DebugOption::WriteFunction);
1249 const auto err = m_format.debugOptions ? generateStack(m_engine) :
QJSValue();
1262 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteLocation)) {
1263 auto pushLocation = [this](QStringView fileName, QStringView lineNumber) {
1264 m_debugMsg += m_format.colors.fileName % skipFilePrefix(fileName) % m_format.colors.reset % u
':' % m_format.colors.lineNumber % lineNumber
1265 % m_format.colors.reset % m_format.colors.debugMsg % u
": "_sv % m_format.colors.reset;
1267 const auto fileName = err.property(u
"fileName"_s);
1269 if (fileName.isUndefined()) {
1270 auto stack2 = requireStack ? stack : m_format.debugOptions ? err.toString() : generateStack(m_engine).toString();
1271 auto stackLine = parseStackLine(stack);
1272 pushLocation(stackLine.fileName, stackLine.lineNumber);
1274 pushLocation(fileName.toString(), err.property(u
"lineNumber"_s).toString());
1279 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteFunction)) {
1280 const QStringView stackView = stack;
1281 const qsizetype pos = stackView.indexOf(
'@'_L1);
1283 m_debugMsg += m_format.colors.program % stackView.first(pos) % m_format.colors.reset % m_format.colors.debugMsg %
": "_L1 % m_format.colors.reset;
1289 m_format.colors.debugMarker % u
"DEBUG:"_sv % m_format.colors.reset % m_format.colors.debugMsg % u
' ' % message % m_format.colors.reset % u
'\n';
1292 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteStackTrace)) {
1293 pushException(m_debugMsg, m_format.colors, stack, u
"| "_sv);
1297 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1298 if (!m_hasDebugMessage && m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation)) {
1301 m_stream << m_debugMsg;
1306 m_hasDebugMessage =
true;
1309void ScriptTester::print(
const QString &message)
1311 if (m_format.debugOptions.testAnyFlags(DebugOption::WriteLocation | DebugOption::WriteFunction)) {
1320 const auto errStr = generateStack(m_engine).toString();
1324 auto stackLine = parseStackLine(err.
sliced(nl + 1));
1327 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteLocation)) {
1328 m_stream << m_format.colors.fileName << skipFilePrefix(stackLine.fileName) << m_format.colors.reset <<
':' << m_format.colors.lineNumber
1329 << stackLine.lineNumber << m_format.colors.reset << m_format.colors.debugMsg <<
": "_L1 << m_format.colors.reset;
1333 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteFunction) && !stackLine.funcName.isEmpty()) {
1334 m_stream << m_format.colors.program << stackLine.funcName << m_format.colors.reset << m_format.colors.debugMsg <<
": "_L1
1335 << m_format.colors.reset;
1341 m_stream << m_format.colors.debugMarker <<
"PRINT:"_L1 << m_format.colors.reset << m_format.colors.debugMsg <<
' ' << message << m_format.colors.reset
1349 const auto path = getModulePath(m_engine, fileName, m_paths.modules, &error);
1354 auto mod = m_engine->importModule(path);
1355 if (mod.isError()) {
1356 m_engine->throwError(mod);
1361void ScriptTester::loadScript(
const QString &fileName)
1364 const auto path = getModulePath(m_engine, fileName, m_paths.scripts, &contentOrError);
1369 if (!
readFile(m_engine, path, &contentOrError, &contentOrError)) {
1374 const QJSValue val = m_engine->evaluate(contentOrError, fileName);
1380 m_engine->throwError(val);
1383bool ScriptTester::startTestCase(
const QString &name,
int nthStack)
1385 if (m_executionConfig.patternType == PatternType::Inactive) {
1389 const bool hasMatch = m_executionConfig.pattern.matchView(name).hasMatch();
1390 const bool exclude = m_executionConfig.patternType == PatternType::Exclude;
1391 if (exclude != hasMatch) {
1403 writeLocation(nthStack);
1405 m_stream << m_format.colors.testName <<
name << m_format.colors.reset <<
": "_L1 << m_format.colors.labelInfo <<
"SKIP"_L1 << m_format.colors.reset <<
'\n';
1407 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1414void ScriptTester::setConfig(
const QJSValue &config)
1416 bool updateConf =
false;
1418#define READ_CONFIG(fn, name) \
1419 fn(config, u"" #name ""_s, [&](auto value) { \
1420 m_editorConfig.name = std::move(value); \
1421 updateConf = true; \
1423 READ_CONFIG(readString, syntax);
1424 READ_CONFIG(readString, indentationMode);
1425 READ_CONFIG(readInt, indentationWidth);
1426 READ_CONFIG(readInt, tabWidth);
1427 READ_CONFIG(readBool, replaceTabs);
1428 READ_CONFIG(readBool, autoBrackets);
1432 m_editorConfig.updated =
false;
1433 m_editorConfig.inherited = m_configStack.empty();
1436#define READ_PLACEHOLDER(name) \
1437 readString(config, u"" #name ""_s, [&](QString s) { \
1438 m_placeholders.name = s.isEmpty() ? u'\0' : s[0]; \
1440 readString(config, u"" #name "2"_s, [&](QString s) { \
1441 m_fallbackPlaceholders.name = s.isEmpty() ? m_format.fallbackPlaceholders.name : s[0]; \
1443 READ_PLACEHOLDER(cursor);
1444 READ_PLACEHOLDER(secondaryCursor);
1445 READ_PLACEHOLDER(virtualText);
1446#undef READ_PLACEHOLDER
1449 readString(config, name, [&](
QString s) {
1452 m_placeholders.*startMem = m_placeholders.*endMem = u
'\0';
1455 m_placeholders.*startMem = m_placeholders.*endMem = s[0];
1458 m_placeholders.*startMem = s[0];
1459 m_placeholders.*endMem = s[1];
1463 readString(config, fallbackName, [&](
QString s) {
1466 m_fallbackPlaceholders.*startMem = m_format.fallbackPlaceholders.*startMem;
1467 m_fallbackPlaceholders.*endMem = m_format.fallbackPlaceholders.*endMem;
1470 m_fallbackPlaceholders.*startMem = m_fallbackPlaceholders.*endMem = s[0];
1473 m_fallbackPlaceholders.*startMem = s[0];
1474 m_fallbackPlaceholders.*endMem = s[1];
1479 readSelection(u
"selection"_s, u
"selection2"_s, &Placeholders::selectionStart, &Placeholders::selectionEnd);
1480 readSelection(u
"secondarySelection"_s, u
"secondarySelection2"_s, &Placeholders::secondarySelectionStart, &Placeholders::secondarySelectionEnd);
1483void ScriptTester::resetConfig()
1485 m_fallbackPlaceholders = m_format.fallbackPlaceholders;
1486 m_placeholders = m_defaultPlaceholders;
1487 m_editorConfig = makeEditorConfig();
1488 m_configStack.clear();
1491void ScriptTester::pushConfig()
1493 m_configStack.emplace_back(Config{m_fallbackPlaceholders, m_placeholders, m_editorConfig});
1494 m_editorConfig.inherited =
true;
1497void ScriptTester::popConfig()
1499 if (m_configStack.empty()) {
1502 auto const &config = m_configStack.back();
1503 m_fallbackPlaceholders = config.fallbackPlaceholders;
1504 m_placeholders = config.placeholders;
1505 const bool updated = m_editorConfig.updated && m_editorConfig.inherited;
1506 m_editorConfig = config.editorConfig;
1507 m_editorConfig.updated = updated;
1508 m_configStack.pop_back();
1514 auto err = m_engine->evaluate(program, u
"(program)"_s, 1, &stack);
1516 m_engine->throwError(err);
1521void ScriptTester::setInput(
const QString &input,
bool blockSelection)
1523 auto err = m_input.setText(input, m_placeholders);
1524 if (err.
isEmpty() && checkMultiCursorCompatibility(m_input, blockSelection, &err)) {
1525 m_input.blockSelection = blockSelection;
1528 m_engine->throwError(err);
1533void ScriptTester::moveExpectedOutputToInput(
bool blockSelection)
1536 std::swap(m_input, m_expected);
1537 reuseInput(blockSelection);
1540void ScriptTester::reuseInput(
bool blockSelection)
1543 if (checkMultiCursorCompatibility(m_input, blockSelection, &err)) {
1544 m_input.blockSelection = blockSelection;
1547 m_engine->throwError(err);
1552bool ScriptTester::reuseInputWithBlockSelection()
1555 if (!checkMultiCursorCompatibility(m_input,
true, &err)) {
1558 m_input.blockSelection =
true;
1563bool ScriptTester::checkMultiCursorCompatibility(
const DocumentText &doc,
bool blockSelection,
QString *err)
1565 if (doc.totalSelection > 1 || doc.totalCursor > 1) {
1566 if (blockSelection) {
1567 *err = u
"blockSelection is incompatible with multi-cursor/selection"_s;
1570 if (m_doc->config()->ovr()) {
1571 *err = u
"overrideMode is incompatible with multi-cursor/selection"_s;
1579void ScriptTester::initDocConfig()
1581 if (m_editorConfig.updated) {
1585 m_editorConfig.updated =
true;
1587 m_view->config()->setValue(KateViewConfig::AutoBrackets, m_editorConfig.autoBrackets);
1589 m_doc->setHighlightingMode(m_editorConfig.syntax);
1591 auto *docConfig = m_doc->config();
1593 docConfig->setIndentationMode(m_editorConfig.indentationMode);
1594 docConfig->setIndentationWidth(m_editorConfig.indentationWidth);
1595 docConfig->setReplaceTabsDyn(m_editorConfig.replaceTabs);
1596 docConfig->setTabWidth(m_editorConfig.tabWidth);
1602void ScriptTester::syncIndenter()
1605 m_doc->removeView(m_view);
1606 m_doc->updateConfig();
1607 m_doc->addView(m_view);
1610void ScriptTester::initInputDoc()
1614 m_doc->setText(m_input.text);
1616 m_view->clearSecondaryCursors();
1617 m_view->setBlockSelection(m_input.blockSelection);
1618 m_view->setSelection(m_input.selection);
1619 m_view->setCursorPosition(m_input.cursor);
1621 if (!m_input.secondaryCursorsWithSelection.isEmpty()) {
1622 m_view->addSecondaryCursorsWithSelection(m_input.secondaryCursorsWithSelection);
1626void ScriptTester::setExpectedOutput(
const QString &expected,
bool blockSelection)
1628 auto err = m_expected.setText(expected, m_placeholders);
1629 if (err.
isEmpty() && checkMultiCursorCompatibility(m_expected, blockSelection, &err)) {
1630 m_expected.blockSelection = blockSelection;
1632 m_engine->throwError(err);
1637void ScriptTester::reuseExpectedOutput(
bool blockSelection)
1640 if (checkMultiCursorCompatibility(m_expected, blockSelection, &err)) {
1641 m_expected.blockSelection = blockSelection;
1643 m_engine->throwError(err);
1648void ScriptTester::copyInputToExpectedOutput(
bool blockSelection)
1650 m_expected = m_input;
1651 reuseExpectedOutput(blockSelection);
1654bool ScriptTester::checkOutput()
1659 m_output.text = m_doc->text();
1660 m_output.totalLine = m_doc->lines();
1661 m_output.blockSelection = m_view->blockSelection();
1662 m_output.cursor = m_view->cursorPosition();
1663 m_output.selection = m_view->selectionRange();
1667 const auto &secondaryCursors = m_view->secondaryCursors();
1668 m_output.secondaryCursors.resize(secondaryCursors.size());
1669 auto it = m_output.secondaryCursors.begin();
1670 for (
const auto &c : secondaryCursors) {
1681 if (m_output.text != m_expected.text || m_output.blockSelection != m_expected.blockSelection) {
1683 }
else if (!m_expected.blockSelection) {
1685 auto cursorEq = [
this](
const Cursor &output,
const Cursor &expected) {
1686 if (output.
line() != expected.line()) {
1689 int lineLen = m_doc->lineLength(expected.line());
1690 int column = qMin(lineLen, expected.column());
1691 return output.
column() == column;
1693 auto rangeEq = [=](
const Range &output,
const Range &expected) {
1694 return cursorEq(output.
start(), expected.start()) && cursorEq(output.
end(), expected.
end());
1696 auto SecondaryEq = [=](
const ViewPrivate::PlainSecondaryCursor &c1,
const ViewPrivate::PlainSecondaryCursor &c2) {
1697 if (!cursorEq(c1.pos, c2.pos) || c1.range.isValid() != c2.range.isValid()) {
1700 return !c1.range.isValid() || rangeEq(c1.range, c2.range);
1703 if (cursorEq(m_output.cursor, m_expected.cursor) && rangeEq(m_output.selection, m_expected.selection)
1704 && std::equal(m_output.secondaryCursors.begin(),
1705 m_output.secondaryCursors.end(),
1706 m_expected.secondaryCursorsWithSelection.constBegin(),
1707 m_expected.secondaryCursorsWithSelection.constEnd(),
1711 }
else if (m_output.cursor == m_expected.cursor && m_output.selection == m_expected.selection
1712 && std::equal(m_output.secondaryCursors.begin(),
1713 m_output.secondaryCursors.end(),
1714 m_expected.secondaryCursorsWithSelection.constBegin(),
1715 m_expected.secondaryCursorsWithSelection.constEnd(),
1716 [](
const ViewPrivate::PlainSecondaryCursor &c1,
const ViewPrivate::PlainSecondaryCursor &c2) {
1717 return c1.pos == c2.pos && c1.range == c2.range;
1729 TextItem::Kind kind;
1732 cursorItems.
resize(m_output.secondaryCursors.size() * 3 + 3);
1734 auto it = cursorItems.
begin();
1735 if (m_output.cursor.isValid()) {
1736 *it++ = {m_output.cursor, TextItem::Cursor};
1738 if (m_output.selection.isValid()) {
1739 const bool isEmptySelection = m_output.selection.isEmpty();
1740 const auto start = isEmptySelection ? TextItem::EmptySelectionStart : TextItem::SelectionStart;
1741 const auto end = isEmptySelection ? TextItem::EmptySelectionEnd : TextItem::SelectionEnd;
1742 *it++ = {m_output.selection.start(),
start};
1743 *it++ = {m_output.selection.end(),
end};
1745 for (
const auto &c : m_output.secondaryCursors) {
1746 *it++ = {c.pos, TextItem::SecondaryCursor};
1747 if (c.range.start().line() != -1) {
1748 const bool isEmptySelection = c.range.isEmpty();
1749 const auto start = isEmptySelection ? TextItem::EmptySecondarySelectionStart : TextItem::SecondarySelectionStart;
1750 const auto end = isEmptySelection ? TextItem::EmptySecondarySelectionEnd : TextItem::SecondarySelectionEnd;
1751 *it++ = {c.range.start(),
start};
1752 *it++ = {c.range.end(),
end};
1756 const auto end = it;
1757 std::sort(cursorItems.
begin(), end, [](
const CursorItem &a,
const CursorItem &b) {
1758 if (a.cursor < b.cursor) {
1761 if (a.cursor > b.cursor) {
1764 return a.kind < b.kind;
1766 it = cursorItems.
begin();
1773 m_output.items.clear();
1774 m_output.hasFormattingItems =
false;
1775 m_output.hasBlockSelectionItems =
false;
1780 auto nextPos = output.
indexOf(u
'\n', pos);
1781 qsizetype lineLen = nextPos == -1 ? output.
size() - pos : nextPos - pos;
1782 int virtualTextLen = 0;
1783 for (; it !=
end && it->cursor.line() == line; ++it) {
1784 virtualTextLen = (it->cursor.column() > lineLen) ? it->cursor.column() - lineLen : 0;
1785 m_output.items.push_back({pos + it->cursor.column() - virtualTextLen, it->kind, virtualTextLen});
1787 if (nextPos == -1) {
1790 m_output.items.push_back({nextPos, TextItem::NewLine, virtualTextLen});
1801bool ScriptTester::incrementCounter(
bool isSuccessNotAFailure,
bool xcheck)
1804 m_successCounter += isSuccessNotAFailure;
1805 m_failureCounter += !isSuccessNotAFailure;
1806 return isSuccessNotAFailure;
1807 }
else if (m_executionConfig.xCheckAsFailure) {
1811 m_xSuccessCounter += !isSuccessNotAFailure;
1812 m_xFailureCounter += isSuccessNotAFailure;
1813 return !isSuccessNotAFailure;
1817void ScriptTester::incrementError()
1822void ScriptTester::incrementBreakOnError()
1824 ++m_breakOnErrorCounter;
1827int ScriptTester::countError()
const
1829 return m_errorCounter + m_failureCounter + m_xFailureCounter;
1832bool ScriptTester::hasTooManyErrors()
const
1834 return m_executionConfig.maxError > 0 && countError() >= m_executionConfig.maxError;
1837int ScriptTester::startTest()
1840 m_hasDebugMessage =
false;
1842 flags |= m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput) ? 1 : 0;
1843 flags |= m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation) ? 2 : 0;
1847void ScriptTester::endTest(
bool ok,
bool showBlockSelection)
1853 constexpr auto mask = TestFormatOptions() | TestFormatOption::AlwaysWriteLocation | TestFormatOption::AlwaysWriteInputOutput;
1854 if ((m_format.testFormatOptions & mask) != TestFormatOption::AlwaysWriteLocation) {
1858 if (showBlockSelection) {
1859 m_stream << m_format.colors.blockSelectionInfo << (m_input.blockSelection ?
" [blockSelection=1]"_L1 :
" [blockSelection=0]"_L1)
1860 << m_format.colors.reset;
1862 m_stream << m_format.colors.success <<
" Ok\n"_L1 << m_format.colors.reset;
1865void ScriptTester::writeTestExpression(
const QString &name,
const QString &type,
int nthStack,
const QString &program)
1873 writeLocation(nthStack);
1875 writeTestName(name);
1877 writeTypeAndProgram(type, program);
1879 m_stream << m_format.colors.reset;
1881 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1886void ScriptTester::writeDualModeAborted(
const QString &name,
int nthStack)
1888 ++m_dualModeAbortedCounter;
1889 writeLocation(nthStack);
1890 writeTestName(name);
1891 m_stream << m_format.colors.error <<
"cmp DUAL_MODE"_L1 << m_format.colors.reset << m_format.colors.blockSelectionInfo <<
" [blockSelection=1]"_L1
1892 << m_format.colors.reset << m_format.colors.error <<
" Aborted\n"_L1 << m_format.colors.reset;
1895void ScriptTester::writeTestName(
const QString &name)
1897 if (!m_format.testFormatOptions.testAnyFlag(TestFormatOption::HiddenTestName) && !
name.
isEmpty()) {
1898 m_stream << m_format.colors.testName <<
name << m_format.colors.reset <<
": "_L1;
1902void ScriptTester::writeTypeAndProgram(
const QString &type,
const QString &program)
1904 m_stream << m_format.colors.error <<
type <<
" `"_L1 << m_format.colors.reset << m_format.colors.program << program << m_format.colors.reset
1905 << m_format.colors.error <<
'`' << m_format.colors.reset;
1908void ScriptTester::writeTestResult(
const QString &name,
1915 const QString &expectedResult,
1918 constexpr int outputIsOk = 1 << 0;
1919 constexpr int containsResultOrError = 1 << 1;
1920 constexpr int expectedErrorButNoError = 1 << 2;
1921 constexpr int expectedNoErrorButError = 1 << 3;
1922 constexpr int isResultNotError = 1 << 4;
1923 constexpr int sameResultOrError = 1 << 5;
1924 constexpr int ignoreInputOutput = 1 << 6;
1926 const bool alwaysWriteTest = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation);
1927 const bool alwaysWriteInputOutput = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput);
1929 const bool outputDiffer = !(options & (outputIsOk | ignoreInputOutput));
1930 const bool resultDiffer = (options & expectedNoErrorButError) || ((options & containsResultOrError) && !(options & sameResultOrError));
1942 if (alwaysWriteTest) {
1943 if (alwaysWriteInputOutput && !outputDiffer && !resultDiffer) {
1944 m_stream << m_format.colors.success <<
" OK"_L1;
1945 }
else if (!m_hasDebugMessage) {
1950 writeLocation(nthStack);
1952 writeTestName(name);
1955 if (outputDiffer && resultDiffer) {
1956 m_stream << m_format.colors.error <<
"Output and Result differs"_L1;
1957 }
else if (resultDiffer) {
1958 m_stream << m_format.colors.error <<
"Result differs"_L1;
1959 }
else if (outputDiffer) {
1960 m_stream << m_format.colors.error <<
"Output differs"_L1;
1961 }
else if (alwaysWriteInputOutput && !alwaysWriteTest) {
1962 m_stream << m_format.colors.success <<
"OK"_L1;
1964 if (!alwaysWriteTest) {
1967 writeTypeAndProgram(type, program);
1971 if (!alwaysWriteTest) {
1972 m_stream << m_format.colors.error;
1974 m_stream <<
" -- "_L1 << msg << m_format.colors.reset;
1975 }
else if (alwaysWriteTest) {
1976 m_stream << m_format.colors.reset;
1979 m_stream << m_format.colors.blockSelectionInfo;
1980 if (m_output.blockSelection == m_expected.blockSelection && m_expected.blockSelection == m_input.blockSelection) {
1981 m_stream << (m_input.blockSelection ?
" [blockSelection=1]"_L1 :
" [blockSelection=0]"_L1);
1983 m_stream <<
" [blockSelection=(input="_L1 << m_input.blockSelection <<
", output="_L1 << m_output.blockSelection <<
", expected="_L1
1984 << m_expected.blockSelection <<
")]"_L1;
1986 m_stream << m_format.colors.reset <<
":\n"_L1;
1991 m_stream << m_debugMsg;
1997 if (!(options & ignoreInputOutput)) {
1998 writeDataTest(options & outputIsOk);
2004 if (options & (containsResultOrError | sameResultOrError)) {
2005 if (!(options & ignoreInputOutput)) {
2006 m_stream <<
" ---------\n"_L1;
2012 if (options & expectedErrorButNoError) {
2013 m_stream << m_format.colors.error <<
" An error is expected, but there is none"_L1 << m_format.colors.reset <<
'\n';
2015 writeLabel(m_stream, m_format.colors,
false,
" result: "_L1);
2016 m_stream << m_format.colors.result << result << m_format.colors.reset <<
'\n';
2018 m_stream <<
" expected: "_L1;
2019 m_stream << m_format.colors.result << expectedResult << m_format.colors.reset <<
'\n';
2025 auto label = (options & (isResultNotError | sameResultOrError)) ?
" result: "_L1 :
" error: "_L1;
2026 writeLabel(m_stream, m_format.colors, options & sameResultOrError, label);
2028 m_stream << m_format.colors.result << result << m_format.colors.reset <<
'\n';
2029 if (!(options & sameResultOrError)) {
2030 auto differPos = computeOffsetDifference(result, expectedResult);
2031 m_stream <<
" expected: "_L1;
2032 m_stream << m_format.colors.result << expectedResult << m_format.colors.reset <<
'\n';
2033 writeCarretLine(m_stream, m_format.colors, differPos + 12);
2041 if (options & expectedNoErrorButError) {
2042 m_stream <<
" ---------\n"_L1 << m_format.colors.error <<
" Uncaught exception: "_L1 << exception.
toString() <<
'\n';
2043 writeException(exception, u
" | "_sv);
2051 const auto stack = getStack(exception);
2052 if (stack.isUndefined()) {
2053 m_stream << m_format.colors.error << prefix <<
"undefined\n"_L1 << m_format.colors.reset;
2055 m_stringBuffer.clear();
2056 pushException(m_stringBuffer, m_format.colors, stack.toString(), prefix);
2057 m_stream << m_stringBuffer;
2061void ScriptTester::writeLocation(
int nthStack)
2063 const auto errStr = generateStack(m_engine).toString();
2067 qsizetype startIndex = 0;
2068 while (nthStack-- > 0) {
2069 qsizetype pos = err.
indexOf(
'\n'_L1, startIndex);
2073 startIndex = pos + 1;
2076 auto stackLine = parseStackLine(err.
sliced(startIndex));
2077 m_stream << m_format.colors.fileName << stackLine.fileName << m_format.colors.reset <<
':' << m_format.colors.lineNumber << stackLine.lineNumber
2078 << m_format.colors.reset <<
": "_L1;
2084struct ScriptTester::Replacements {
2085 const QChar selectionPlaceholders[2];
2086 const QChar secondarySelectionPlaceholders[2];
2087 const QChar virtualTextPlaceholder;
2090 QStringView replacements[TextItem::MaxElement][2];
2093 static constexpr int tabBufferLen = 16;
2094 QChar tabBuffer[tabBufferLen];
2096#define GET_CH_PLACEHOLDER(name, b) (placeholders.name != u'\0' && placeholders.name != u'\n' && b ? placeholders.name : fallbackPlaceholders.name)
2097#define GET_PLACEHOLDER(name, b) QStringView(&GET_CH_PLACEHOLDER(name, b), 1)
2099 Replacements(
const Colors &colors,
const Placeholders &placeholders,
const Placeholders &fallbackPlaceholders)
2100 : selectionPlaceholders{GET_CH_PLACEHOLDER(selectionStart, true), GET_CH_PLACEHOLDER(selectionEnd, true)}
2101 , secondarySelectionPlaceholders{GET_CH_PLACEHOLDER(secondarySelectionStart, true), GET_CH_PLACEHOLDER(secondarySelectionEnd, true)}
2102 , virtualTextPlaceholder(GET_PLACEHOLDER(virtualText,
2103 placeholders.virtualText != placeholders.cursor && placeholders.virtualText != placeholders.selectionStart
2104 && placeholders.virtualText != placeholders.selectionEnd)[0])
2106 replacements[TextItem::EmptySelectionStart][0] = colors.selection;
2107 replacements[TextItem::EmptySelectionStart][1] = selectionPlaceholders;
2108 replacements[TextItem::EmptySecondarySelectionStart][0] = colors.secondarySelection;
2109 replacements[TextItem::EmptySecondarySelectionStart][1] = secondarySelectionPlaceholders;
2112 replacements[TextItem::SecondarySelectionStart][0] = colors.secondarySelection;
2113 replacements[TextItem::SecondarySelectionStart][1] = {&secondarySelectionPlaceholders[0], 1};
2114 replacements[TextItem::SecondarySelectionEnd][0] = colors.secondarySelection;
2115 replacements[TextItem::SecondarySelectionEnd][1] = {&secondarySelectionPlaceholders[1], 1};
2117 replacements[TextItem::Cursor][0] = colors.cursor;
2118 replacements[TextItem::Cursor][1] =
2119 GET_PLACEHOLDER(cursor, selectionPlaceholders[0] != placeholders.cursor && selectionPlaceholders[1] != placeholders.cursor);
2121 replacements[TextItem::SecondaryCursor][0] = colors.secondaryCursor;
2122 replacements[TextItem::SecondaryCursor][1] = GET_PLACEHOLDER(
2124 selectionPlaceholders[0] != placeholders.secondaryCursor && selectionPlaceholders[1] != placeholders.secondaryCursor
2125 && secondarySelectionPlaceholders[0] != placeholders.secondaryCursor && secondarySelectionPlaceholders[1] != placeholders.secondaryCursor);
2127 replacements[TextItem::SelectionStart][0] = colors.selection;
2128 replacements[TextItem::SelectionEnd][0] = colors.selection;
2130 replacements[TextItem::BlockSelectionStart][0] = colors.blockSelection;
2131 replacements[TextItem::BlockSelectionEnd][0] = colors.blockSelection;
2132 replacements[TextItem::VirtualBlockCursor][0] = colors.blockSelection;
2133 replacements[TextItem::VirtualBlockSelectionStart][0] = colors.blockSelection;
2134 replacements[TextItem::VirtualBlockSelectionEnd][0] = colors.blockSelection;
2137#undef GET_CH_PLACEHOLDER
2138#undef GET_PLACEHOLDER
2140 void initEscapeForDoubleQuote(
const Colors &colors)
2142 replacements[TextItem::NewLine][0] = colors.resultReplacement;
2143 replacements[TextItem::NewLine][1] = u
"\\n"_sv;
2144 replacements[TextItem::Tab][0] = colors.resultReplacement;
2145 replacements[TextItem::Tab][1] = u
"\\t"_sv;
2146 replacements[TextItem::Backslash][0] = colors.resultReplacement;
2147 replacements[TextItem::Backslash][1] = u
"\\\\"_sv;
2148 replacements[TextItem::DoubleQuote][0] = colors.resultReplacement;
2149 replacements[TextItem::DoubleQuote][1] = u
"\\\""_sv;
2152 void initReplaceNewLineAndTabWithLiteral(
const Colors &colors)
2154 replacements[TextItem::NewLine][0] = colors.resultReplacement;
2155 replacements[TextItem::NewLine][1] = u
"\\n"_sv;
2156 replacements[TextItem::Tab][0] = colors.resultReplacement;
2157 replacements[TextItem::Tab][1] = u
"\\t"_sv;
2160 void initNewLine(
const Format &format)
2162 const auto &newLine = format.textReplacement.newLine;
2163 replacements[TextItem::NewLine][0] = format.colors.resultReplacement;
2164 replacements[TextItem::NewLine][1] = newLine.unicode() ? QStringView(&newLine, 1) : u
"↵"_sv;
2167 void initTab(
const Format &format, DocumentPrivate *doc)
2169 const auto &repl = format.textReplacement;
2170 tabWidth = qMin(doc->config()->tabWidth(), tabBufferLen);
2172 for (
int i = 0; i < tabWidth - 1; ++i) {
2173 tabBuffer[i] = repl.tab1;
2175 tabBuffer[tabWidth - 1] = repl.tab2;
2177 replacements[TextItem::Tab][0] = format.colors.resultReplacement;
2178 replacements[TextItem::Tab][1] = QStringView(tabBuffer, tabWidth);
2181 void initSelections(
bool hasVirtualBlockSelection,
bool reverseSelection)
2183 if (hasVirtualBlockSelection && reverseSelection) {
2184 replacements[TextItem::SelectionStart][1] = {&selectionPlaceholders[1], 1};
2185 replacements[TextItem::SelectionEnd][1] = {&selectionPlaceholders[0], 1};
2187 replacements[TextItem::BlockSelectionStart][1] = {&selectionPlaceholders[0], 1};
2188 replacements[TextItem::BlockSelectionEnd][1] = {&selectionPlaceholders[1], 1};
2190 replacements[TextItem::SelectionStart][1] = {&selectionPlaceholders[0], 1};
2191 replacements[TextItem::SelectionEnd][1] = {&selectionPlaceholders[1], 1};
2192 if (hasVirtualBlockSelection) {
2193 replacements[TextItem::BlockSelectionStart][1] = {&selectionPlaceholders[1], 1};
2194 replacements[TextItem::BlockSelectionEnd][1] = {&selectionPlaceholders[0], 1};
2198 if (hasVirtualBlockSelection) {
2199 replacements[TextItem::VirtualBlockCursor][1] = replacements[TextItem::Cursor][1];
2200 replacements[TextItem::VirtualBlockSelectionStart][1] = {&selectionPlaceholders[0], 1};
2201 replacements[TextItem::VirtualBlockSelectionEnd][1] = {&selectionPlaceholders[1], 1};
2203 replacements[TextItem::BlockSelectionStart][1] = QStringView();
2204 replacements[TextItem::BlockSelectionEnd][1] = QStringView();
2205 replacements[TextItem::VirtualBlockCursor][1] = QStringView();
2206 replacements[TextItem::VirtualBlockSelectionStart][1] = QStringView();
2207 replacements[TextItem::VirtualBlockSelectionEnd][1] = QStringView();
2211 const QStringView (&
operator[](TextItem::Kind i)
const)[2]
2213 return replacements[i];
2217void ScriptTester::writeDataTest(
bool outputIsOk)
2219 Replacements replacements(m_format.colors, m_placeholders, m_fallbackPlaceholders);
2221 const auto textFormat = (m_input.blockSelection || m_output.blockSelection || (outputIsOk && m_expected.blockSelection))
2222 ? m_format.documentTextFormatWithBlockSelection
2223 : m_format.documentTextFormat;
2225 bool alignNL =
true;
2227 switch (textFormat) {
2228 case DocumentTextFormat::Raw:
2230 case DocumentTextFormat::EscapeForDoubleQuote:
2231 replacements.initEscapeForDoubleQuote(m_format.colors);
2234 case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral:
2235 replacements.initReplaceNewLineAndTabWithLiteral(m_format.colors);
2238 case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder:
2239 replacements.initNewLine(m_format);
2241 case DocumentTextFormat::ReplaceTabWithPlaceholder:
2242 replacements.initTab(m_format, m_doc);
2246 auto writeText = [&](
const DocumentText &docText, qsizetype carretLine = -1, qsizetype carretColumn = -1,
bool lastCall =
false) {
2247 const bool hasVirtualBlockSelection = (docText.blockSelection && docText.selection.start().line() != -1);
2249 const QStringView inSelectionFormat = (hasVirtualBlockSelection && docText.selection.columnWidth() == 0) ?
QStringView() : m_format.colors.inSelection;
2251 replacements.initSelections(hasVirtualBlockSelection, docText.selection.columnWidth() < 0);
2254 bool showCarret = (carretColumn != -1);
2256 qsizetype previousLinePos = 0;
2257 qsizetype virtualTabLen = 0;
2258 qsizetype textPos = 0;
2259 qsizetype virtualTextLen = 0;
2260 for (
const TextItem &item : docText.items) {
2262 if (textPos != item.pos) {
2263 auto textFragment =
QStringView(docText.text).
sliced(textPos, item.pos - textPos);
2264 m_stream << m_format.colors.result << inSelection << textFragment << m_format.colors.reset;
2268 if (virtualTextLen < item.virtualTextLen && docText.blockSelection) {
2269 m_stream << m_format.colors.reset << m_format.colors.virtualText << inSelection;
2270 m_stream.setPadChar(replacements.virtualTextPlaceholder);
2271 m_stream.setFieldWidth(item.virtualTextLen - virtualTextLen);
2273 m_stream.setFieldWidth(0);
2274 if (!m_format.colors.virtualText.isEmpty() || !inSelection.
isEmpty()) {
2275 m_stream << m_format.colors.reset;
2277 textPos = item.pos + item.isCharacter();
2278 virtualTextLen = item.virtualTextLen;
2282 const bool isInSelection = !inSelection.
isEmpty();
2283 if (isInSelection && item.isSelection(hasVirtualBlockSelection)) {
2288 const auto &replacement = replacements[item.kind];
2289 if (!replacement[1].isEmpty()) {
2290 m_stream << replacement[0] << inSelection;
2296 if (item.kind == TextItem::Tab && replacements.tabWidth) {
2297 const auto column = item.pos - previousLinePos + virtualTabLen;
2298 const auto skip = column % replacements.tabWidth;
2299 virtualTabLen += replacement[1].
size() - skip - 1;
2300 m_stream << replacement[1].sliced(skip);
2302 m_stream << replacement[1];
2306 const bool insertNewLine = (alignNL && item.kind == TextItem::NewLine);
2307 if (insertNewLine || (!replacement[1].isEmpty() && (!replacement[0].isEmpty() || !inSelection.isEmpty()))) {
2308 m_stream << m_format.colors.reset;
2309 if (insertNewLine) {
2311 if (showCarret && carretLine == line) {
2313 writeCarretLine(m_stream, m_format.colors, carretColumn);
2319 if (item.kind == TextItem::NewLine) {
2322 previousLinePos = item.pos + 1;
2326 if (!isInSelection && item.isSelection(hasVirtualBlockSelection)) {
2327 inSelection = inSelectionFormat;
2330 textPos = item.pos + item.isCharacter();
2334 if (textPos != docText.text.size()) {
2335 m_stream << m_format.colors.result <<
QStringView(docText.text).
sliced(textPos) << m_format.colors.reset;
2341 writeCarretLine(m_stream, m_format.colors, carretColumn);
2342 }
else if (alignNL && docText.totalLine > 1 && !lastCall) {
2347 m_input.insertFormattingItems(textFormat);
2348 writeLabel(m_stream, m_format.colors, outputIsOk,
" input: "_L1);
2351 m_expected.insertFormattingItems(textFormat);
2352 writeLabel(m_stream, m_format.colors, outputIsOk,
" output: "_L1);
2354 writeText(m_expected);
2356 m_output.insertFormattingItems(textFormat);
2361 qsizetype carretLine = 0;
2362 qsizetype carretColumn = 0;
2363 qsizetype ignoredLen = 0;
2364 auto differPos = computeOffsetDifference(m_output.text, m_expected.text);
2365 auto it1 = m_output.items.begin();
2366 auto it2 = m_expected.items.begin();
2367 while (it1 != m_output.items.end() && it2 != m_expected.items.end()) {
2368 if (!m_output.blockSelection && it1->isBlockSelectionOrVirtual()) {
2372 if (!m_expected.blockSelection && it2->isBlockSelectionOrVirtual()) {
2377 if (differPos <= it1->pos || it1->pos != it2->pos || it1->kind != it2->kind
2378 || it1->virtualTextLen != (m_expected.blockSelection ? it2->virtualTextLen : 0)) {
2382 carretColumn += it1->virtualTextLen + replacements[it1->kind][1].
size() - it1->isCharacter();
2383 if (alignNL && it1->kind == TextItem::NewLine) {
2386 ignoredLen = it1->pos + 1;
2392 if (it1 != m_output.items.end() && it1->pos < differPos) {
2393 differPos = it1->pos;
2395 if (it2 != m_expected.items.end() && it2->pos < differPos) {
2396 differPos = it2->pos;
2399 carretColumn += 12 + differPos - ignoredLen;
2404 const bool insertCarretOnOutput = (alignNL && (m_output.totalLine > 1 || m_expected.totalLine > 1));
2405 writeText(m_output, carretLine, insertCarretOnOutput ? carretColumn : -1);
2406 m_stream <<
" expected: "_L1;
2407 writeText(m_expected, carretLine, carretColumn,
true);
2411void ScriptTester::writeSummary()
2413 auto &colors = m_format.colors;
2415 if (m_failureCounter || m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation)) {
2419 if (m_skipedCounter || m_breakOnErrorCounter) {
2420 m_stream << colors.labelInfo <<
"Test cases: Skipped: "_L1 << m_skipedCounter <<
" Aborted: "_L1 << m_breakOnErrorCounter << colors.reset <<
'\n';
2423 m_stream <<
"Success: "_L1 << colors.success << m_successCounter << colors.reset <<
" Failure: "_L1 << (m_failureCounter ? colors.error : colors.success)
2424 << m_failureCounter << colors.reset;
2426 if (m_dualModeAbortedCounter) {
2427 m_stream <<
" DUAL_MODE aborted: "_L1 << colors.error << m_dualModeAbortedCounter << colors.reset;
2430 if (m_errorCounter) {
2431 m_stream <<
" Error: "_L1 << colors.error << m_errorCounter << colors.reset;
2434 if (m_xSuccessCounter || m_xFailureCounter) {
2435 m_stream <<
" Expected failure: "_L1 << m_xSuccessCounter;
2436 if (m_xFailureCounter) {
2437 m_stream <<
" Unexpected success: "_L1 << colors.error << m_xFailureCounter << colors.reset;
2442void ScriptTester::resetCounters()
2444 m_successCounter = 0;
2445 m_failureCounter = 0;
2446 m_xSuccessCounter = 0;
2447 m_xFailureCounter = 0;
2448 m_skipedCounter = 0;
2450 m_breakOnErrorCounter = 0;
2453void ScriptTester::type(
const QString &str)
2455 m_doc->typeChars(m_view, str);
2458void ScriptTester::enter()
2460 m_doc->newLine(m_view);
2463void ScriptTester::paste(
const QString &str)
2465 m_doc->paste(m_view, str);
2468bool ScriptTester::testIndentFiles(
const QString &name,
const QString &dataDir,
int nthStack,
bool exitOnError)
2475 File(
const QString &path)
2480 text = QTextStream(&file).readAll();
2483 text = file.errorString();
2488 auto openError = [
this](
const QString &msg) {
2499 const QDir testDir(dirPath);
2500 if (!testDir.exists()) {
2501 return openError(testDir.path() + u
" does not exist"_sv);
2518 const auto variablesLen = variables.
size();
2519 bool hasVariable = variablesLen;
2527 const auto type = u
"indent"_s;
2528 const auto program = u
"view.align(document.documentRange())"_s;
2530 bool hasEntry =
false;
2533 for (
const auto &info : testList) {
2536 m_hasDebugMessage =
false;
2538 const auto baseName = info.baseName();
2541 if (!startTestCase(name2, nthStack)) {
2545 auto writeTestName = [&] {
2546 if (!m_format.testFormatOptions.testAnyFlag(TestFormatOption::HiddenTestName)) {
2547 m_stream << m_format.colors.testName <<
name << m_format.colors.reset <<
':' << m_format.colors.testName << baseName << m_format.colors.reset
2552 const bool alwaysWriteTest = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation);
2553 if (alwaysWriteTest) {
2554 writeLocation(nthStack);
2556 writeTypeAndProgram(type, program);
2558 m_stream << m_format.colors.reset <<
' ';
2560 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
2569 const auto dir = info.absoluteFilePath();
2570 const File inputFile(dir + u
"/origin"_sv);
2571 const File expectedFile(dir + u
"/expected"_sv);
2572 if (!inputFile.ok) {
2573 return openError(inputFile.path + u
": " + inputFile.text);
2575 if (!expectedFile.ok) {
2576 return openError(expectedFile.path + u
": " + expectedFile.text);
2585 m_doc->setText(inputFile.text);
2590 auto appendVars = [&](
int i) {
2591 const auto line = m_doc->line(i);
2593 variables += line + u
';';
2596 const auto lines = m_doc->lines();
2597 for (
int i = 0; i < qMin(9, lines); ++i) {
2601 for (
int i = qMax(10, lines - 10); i < lines; ++i) {
2612 m_doc->setVariable(u
""_s, variables);
2614 variables.
resize(variablesLen);
2622 const auto selection = m_doc->documentRange();
2624 m_view->setSelection(selection);
2625 m_doc->align(m_view, selection);
2631 const auto output = m_doc->text();
2632 const bool ok = output == expectedFile.text;
2633 const bool alwaysWriteInputOutput = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput);
2635 if (!alwaysWriteTest && (alwaysWriteInputOutput || !ok)) {
2636 writeLocation(nthStack);
2639 if (!ok || alwaysWriteTest || alwaysWriteInputOutput) {
2641 m_stream << m_format.colors.success <<
"OK\n"_L1 << m_format.colors.reset;
2643 m_stream << m_format.colors.error <<
"Output differs\n"_L1 << m_format.colors.reset;
2646 if (!alwaysWriteTest && (alwaysWriteInputOutput || !ok)) {
2647 writeTypeAndProgram(type, program);
2648 m_stream <<
": \n"_L1;
2650 if (!ok || alwaysWriteInputOutput) {
2651 m_stream << m_debugMsg;
2659 const QString resultPath =
dir + u
"/actual"_sv;
2665 QFile outFile(resultPath);
2667 return openError(resultPath + u
": "_sv + outFile.errorString());
2675 if (!m_diffCmdLoaded) {
2677 m_diffCmdLoaded =
true;
2679 if (!m_diffCmd.path.isEmpty()) {
2683 m_diffCmd.args.push_back(expectedFile.path);
2684 m_diffCmd.args.push_back(resultPath);
2685 proc.
start(m_diffCmd.path, m_diffCmd.args);
2686 m_diffCmd.args.resize(m_diffCmd.args.size() - 2);
2690 m_engine->throwError(u
"diff command error"_s);
2698 qDebug() <<
"Trivial differences output as the 'diff' executable is not in the PATH";
2699 m_stream <<
"--- "_L1 << expectedFile.path <<
"\n+++ " << resultPath <<
'\n';
2702 const auto minLine = qMin(expectedLines.size(), outputLines.size());
2704 for (; i < minLine; ++i) {
2705 if (expectedLines[i] == outputLines[i]) {
2706 m_stream <<
" "_L1 << expectedLines[i] <<
'\n';
2708 m_stream <<
"- "_L1 << expectedLines[i] <<
"\n+ "_L1 << outputLines[i] <<
'\n';
2711 if (expectedLines.size() != outputLines.size()) {
2712 const auto &lines = expectedLines.size() < outputLines.size() ? outputLines : expectedLines;
2713 const auto &prefix = expectedLines.
size() < outputLines.size() ?
"+ "_L1 :
"- "_L1;
2714 const auto maxLine = lines.size();
2715 for (; i < maxLine; ++i) {
2716 m_stream << prefix << lines[i] <<
'\n';
2721 if (exitOnError || hasTooManyErrors()) {
2731 m_engine->throwError(testDir.path() + u
" is empty"_sv);
2735 m_editorConfig.updated = !hasVariable;
2742#include "moc_scripttester_p.cpp"
The Cursor represents a position in a Document.
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
The Cursor represents a position in a Document.
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Backend of KTextEditor::Document related public KTextEditor interfaces.
static constexpr Range invalid() noexcept
Returns an invalid range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
static constexpr Range invalid() noexcept
Returns an invalid range.
Q_SCRIPTABLE QString start(QString train="")
Q_SCRIPTABLE Q_NOREPLY void start()
QString fullName(const PartType &type)
QString name(GameStandardAction id)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QString path(const QString &relativePath)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QString label(StandardShortcut id)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
bool readFile(const QString &sourceUrl, QString &sourceCode)
read complete file contents, helper
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
bool isRelative() const const
void throwError(QJSValue::ErrorType errorType, const QString &message)
bool isError() const const
QJSValue property(const QString &name) const const
QString toString() const const
qsizetype size() const const
bool isEmpty() const const
int exitCode() const const
void setProcessChannelMode(ProcessChannelMode mode)
void start(OpenMode mode)
bool waitForFinished(int msecs)
QString findExecutable(const QString &executableName, const QStringList &paths)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
void resize(qsizetype newSize, QChar fillChar)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QChar first() const const
qsizetype indexOf(QChar c, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar c, Qt::CaseSensitivity cs) const const
qsizetype size() const const
QStringView sliced(qsizetype pos) const const
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar ch) const const
QString toString() const const
bool readLineInto(QString *line, qint64 maxlen)
void setFieldWidth(int width)
void setPadChar(QChar ch)
void resize(qsizetype size)