8#include "katedocument.h"
9#include "katepartdebug.h"
11#include "scripttester_p.h"
19#include <QLatin1StringView>
21#include <QStandardPaths>
22#include <QVarLengthArray>
35constexpr QStringView operator""_sv(
const char16_t *str,
size_t size)
noexcept
51 for (
const QString &dir : dirs) {
58 *
error = u
"file '%1' not found in %2"_sv.arg(name, dirs.join(u
", "_sv));
76 for (
const QString &dir : dirs) {
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));
109 QFile file(sourceUrl);
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;
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());
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
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) {
249 return str.
sliced(filePrefixLen(str));
257 return exception.
property(u
"stack"_s);
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);
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;
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,
1175 DocumentPrivate *doc,
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) {
1673 .range = c.range ? c.range->toRange() : Range::invalid(),
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)
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"
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
static constexpr Range invalid() noexcept
Returns an invalid range.
Q_SCRIPTABLE Q_NOREPLY void start()
Type type(const QSqlDatabase &db)
QString fullName(const PartType &type)
QString path(const QString &relativePath)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QString name(StandardAction id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
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)