KTextEditor

scripttester.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Jonathan Poelen <jonathan.poelen@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "kateconfig.h"
8#include "katedocument.h"
9#include "katepartdebug.h"
10#include "kateview.h"
11#include "scripttester_p.h"
12
13#include <algorithm>
14
15#include <QDir>
16#include <QFile>
17#include <QFileInfo>
18#include <QJSEngine>
19#include <QLatin1StringView>
20#include <QProcess>
21#include <QStandardPaths>
22#include <QVarLengthArray>
23
24namespace KTextEditor
25{
26
27using namespace Qt::Literals::StringLiterals;
28
29namespace
30{
31
32/**
33 * UDL for QStringView
34 */
35constexpr QStringView operator""_sv(const char16_t *str, size_t size) noexcept
36{
37 return QStringView(str, size);
38}
39
40/**
41 * Search for a file \p name in the folder list \p dirs.
42 * @param engine[out] used for set an exception
43 * @param name name if file
44 * @param dirs list of folders to search for the file
45 * @param error[out] used for set an error
46 * @return the path of the file found. Otherwise, an empty string is returned,
47 * an error is set in \p error and an exception in \p engine.
48 */
49static QString getPath(QJSEngine *engine, const QString &name, const QStringList &dirs, QString *error)
50{
51 for (const QString &dir : dirs) {
52 QString path = dir % u'/' % name;
53 if (QFile::exists(path)) {
54 return path;
55 }
56 }
57
58 *error = u"file '%1' not found in %2"_sv.arg(name, dirs.join(u", "_sv));
59 engine->throwError(QJSValue::URIError, *error);
60 return QString();
61}
62
63/**
64 * Same as \c getPath, but also searches in current working directory.
65 * @param engine[out] used for set an exception
66 * @param fileName name if file
67 * @param dirs list of folders to search for the file
68 * @param error[out] used for set an error
69 * @return the path of the file found. Otherwise, an empty string is returned,
70 * an error is set in \p error and an exception in \p engine.
71 */
72static QString getModulePath(QJSEngine *engine, const QString &fileName, const QStringList &dirs, QString *error)
73{
74 if (!dirs.isEmpty()) {
75 if (QFileInfo(fileName).isRelative()) {
76 for (const QString &dir : dirs) {
77 QString path = dir % u'/' % fileName;
78 if (QFile::exists(path)) {
79 return path;
80 }
81 }
82 }
83 }
84
85 if (QFile::exists(fileName)) {
86 return fileName;
87 }
88
89 if (dirs.isEmpty()) {
90 *error = u"file '%1' not found in working directory"_sv.arg(fileName);
91 } else {
92 *error = u"file '%1' not found in %2 and working directory"_sv.arg(fileName, dirs.join(u", "_sv));
93 }
94
95 engine->throwError(QJSValue::URIError, *error);
96 return QString();
97}
98
99/**
100 * Search for a file \p name in the folder list \p dirs.
101 * @param engine[out] used for set an exception
102 * @param sourceUrl file path
103 * @param content[out] file contents
104 * @param error[out] used for set an error
105 * @return \c true when reading is complete, otherwise \c false
106 */
107static bool readFile(QJSEngine *engine, const QString &sourceUrl, QString *content, QString *error)
108{
109 QFile file(sourceUrl);
110 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
111 QTextStream stream(&file);
112 *content = stream.readAll();
113 return true;
114 }
115
116 *error = u"reading error for '%1': %2"_sv.arg(sourceUrl, file.errorString());
117 engine->throwError(QJSValue::URIError, *error);
118 return false;
119}
120
121/**
122 * Write a line with "^~~" at the position \c column.
123 */
124static void writeCarretLine(QTextStream &stream, const ScriptTester::Colors &colors, int column)
125{
126 stream.setPadChar(u' ');
127 stream.setFieldWidth(column);
128 stream << ""_L1;
129 stream.setFieldWidth(0);
130 stream << colors.carret;
131 stream << "^~~"_L1;
132 stream << colors.reset;
133 stream << '\n';
134}
135
136/**
137 * Write a label and adds color when \p colored is \c true.
138 */
139static void writeLabel(QTextStream &stream, const ScriptTester::Colors &colors, bool colored, QLatin1StringView text)
140{
141 if (colored) {
142 stream << colors.labelInfo << text << colors.reset;
143 } else {
144 stream << text;
145 }
146}
147
148/**
149 * When property \p name of \p obj is set, convert it to a string and call \p setFn.
150 */
151template<class SetFn>
152static void readString(const QJSValue &obj, const QString &name, SetFn &&setFn)
153{
154 auto value = obj.property(name);
155 if (!value.isUndefined()) {
156 setFn(value.toString());
157 }
158}
159
160/**
161 * When property \p name of \p obj is set, convert it to a int and call \p setFn.
162 */
163template<class SetFn>
164static void readInt(const QJSValue &obj, const QString &name, SetFn &&setFn)
165{
166 auto value = obj.property(name);
167 if (!value.isUndefined()) {
168 setFn(value.toInt());
169 }
170}
171
172/**
173 * When property \p name of \p obj is set, convert it to a bool and call \p setFn.
174 */
175template<class SetFn>
176static void readBool(const QJSValue &obj, const QString &name, SetFn &&setFn)
177{
178 auto value = obj.property(name);
179 if (!value.isUndefined()) {
180 setFn(value.toBool());
181 }
182}
183
184/**
185 * @return the position where \p a differs from \p b.
186 */
187static qsizetype computeOffsetDifference(QStringView a, QStringView b)
188{
189 qsizetype n = qMin(a.size(), b.size());
190 qsizetype i = 0;
191 for (; i < n; ++i) {
192 if (a[i] != b[i]) {
193 if (a[i].isLowSurrogate() && b[i].isLowSurrogate()) {
194 return qMax(0, i - 1);
195 }
196 return i;
197 }
198 }
199 return i;
200}
201
202struct VirtualText {
203 qsizetype pos1;
204 qsizetype pos2;
205
206 qsizetype size() const
207 {
208 return pos2 - pos1;
209 }
210};
211
212/**
213 * Search for the next element representing virtual text.
214 * If none are found, pos1 and pos2 are set to -1.
215 */
216static VirtualText findVirtualText(QStringView str, qsizetype pos, QChar c)
217{
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) {
222 ++res.pos2;
223 }
224 }
225 return res;
226};
227
228/**
229 * @return the length of the prefix added by Qt when a file is displayed in a js exception.
230 */
231static qsizetype filePrefixLen(QStringView str)
232{
233 // skip file prefix
234 if (str.startsWith("file://"_L1)) {
235 return 7;
236 } else if (str.startsWith("file:"_L1)) {
237 return 5;
238 } else if (str.startsWith("qrc:"_L1)) {
239 return 3;
240 }
241 return 0;
242}
243
244/**
245 * @return \p str without its file prefix (\see filePrefixLen).
246 */
247static QStringView skipFilePrefix(QStringView str)
248{
249 return str.sliced(filePrefixLen(str));
250}
251
252/**
253 * @return stack property of js Error.
254 */
255static inline QJSValue getStack(const QJSValue &exception)
256{
257 return exception.property(u"stack"_s);
258}
259
260/**
261 * @return stack of \p engine.
262 */
263static inline QJSValue generateStack(QJSEngine *engine)
264{
265 engine->throwError(QString());
266 return getStack(engine->catchError());
267}
268
269struct StackLine {
270 QStringView funcName;
271 QStringView filePrefixOrMessage;
272 QStringView fileName;
273 QStringView lineNumber;
274 QStringView remaining;
275};
276
277/**
278 * Parse a line contained in the stack property of a javascript error.
279 * @return the first line. The rest is in \c StackLine::remaining
280 */
281static StackLine parseStackLine(QStringView stack)
282{
283 // format: funcName? '@file:' '//'? fileName ':' lineNumber '\n'
284
285 StackLine ret;
286
287 // func
288 qsizetype pos = stack.indexOf('@'_L1);
289 if (pos >= 0) {
290 ret.funcName = stack.first(pos);
291 stack = stack.sliced(pos + 1);
292 }
293
294 // remove file prefix
295 pos = filePrefixLen(stack);
296 ret.filePrefixOrMessage = stack.first(pos);
297
298 auto endLine = stack.indexOf('\n'_L1, pos);
299 auto line = stack.sliced(pos, ((endLine < 0) ? stack.size() : endLine) - pos);
300 // fileName and lineNumber
301 auto i = line.lastIndexOf(':'_L1);
302 if (i > 0) {
303 ret.fileName = line.sliced(0, i);
304 ret.lineNumber = line.sliced(i + 1);
305 } else {
306 ret.filePrefixOrMessage = line;
307 }
308
309 if (endLine >= 0) {
310 ret.remaining = stack.sliced(endLine + 1);
311 }
312
313 return ret;
314}
315
316/**
317 * Add a formatted error stack to \p buffer.
318 * @param buffer[out]
319 * @param colors
320 * @param stack stack of js Error.
321 * @param prefix prefix added to each start of line
322 */
323static void pushException(QString &buffer, ScriptTester::Colors &colors, QStringView stack, QStringView prefix)
324{
325 // skips the first line that refers to the internal call
326 if (!stack.isEmpty() && stack[0] == u'%') {
327 auto pos = stack.indexOf('\n'_L1);
328 if (pos < 0) {
329 buffer += colors.error % prefix % stack % colors.reset % u'\n';
330 return;
331 }
332 stack = stack.sliced(pos + 1);
333 }
334
335 // color lines
336 while (!stack.isEmpty()) {
337 auto stackLine = parseStackLine(stack);
338 // clang-format off
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
345 % u'\n';
346 // clang-format on
347 stack = stackLine.remaining;
348 }
349}
350
351static inline bool cursorSameAsSecondary(const ScriptTester::Placeholders &placeholders)
352{
353 return placeholders.cursor == placeholders.secondaryCursor;
354}
355
356static inline bool selectionStartSameAsSecondary(const ScriptTester::Placeholders &placeholders)
357{
358 return placeholders.selectionStart == placeholders.selectionStart;
359}
360
361static inline bool selectionEndSameAsSecondary(const ScriptTester::Placeholders &placeholders)
362{
363 return placeholders.selectionEnd == placeholders.selectionEnd;
364}
365
366static inline bool selectionSameAsSecondary(const ScriptTester::Placeholders &placeholders)
367{
368 return selectionStartSameAsSecondary(placeholders) || selectionEndSameAsSecondary(placeholders);
369}
370
371} // anonymous namespace
372
373ScriptTester::EditorConfig ScriptTester::makeEditorConfig()
374{
375 return {
376 .syntax = u"None"_s,
377 .indentationMode = u"none"_s,
378 .indentationWidth = 4,
379 .tabWidth = 4,
380 .replaceTabs = false,
381 .autoBrackets = false,
382 .updated = false,
383 .inherited = false,
384 };
385}
386
387/**
388 * Represents a textual (new line, etc.) or non-textual (cursor, etc.) element
389 * in the text \c DocumentText::text.
390 */
391struct ScriptTester::TextItem {
392 /*
393 * *BlockSelection* are the borders of a block selection and are inserted
394 * before display.
395 *
396 * In scenario 1 and 2, the position of BlockSelection* and Selection* is
397 * reversed.
398 *
399 * Scenario 1: start.column < end.column
400 * input: ...[ssssss...\n...ssssss...\n...ssssss]...
401 * display: ...[ssssss]...\n...[ssssss]...\n...[ssssss]...
402 *
403 * ...[ssssss]...
404 * ~ SelectionStart
405 * ~ BlockSelectionStart
406 * ...[ssssss]...
407 * ~ VirtualBlockSelectionStart
408 * ~ VirtualBlockSelectionEnd
409 * ...[ssssss]...
410 * ~ BlockSelectionEnd
411 * ~ SelectionEnd
412 *
413 * Scenario 2: start.column > end.column
414 * input: ...ssssss[...\n...ssssss...\n...]ssssss...
415 * display: ...[ssssss]...\n...[ssssss]...\n...[ssssss]...
416 *
417 * ...[ssssss]...
418 * ~ BlockSelectionStart
419 * ~ SelectionStart
420 * ...[ssssss]...
421 * ~ VirtualBlockSelectionStart
422 * ~ VirtualBlockSelectionEnd
423 * ...[ssssss]...
424 * ~ SelectionEnd
425 * ~ BlockSelectionEnd
426 *
427 * Scenario 3: start.column == end.column
428 * input: ...[...\n......\n...]...
429 * display: ...[...\n...|...\n...]...
430 *
431 * ...[...
432 * ~ SelectionStart
433 * ...|...
434 * ~ VirtualBlockCursor
435 * ...]...
436 * ~ SelectionEnd
437 */
438 enum Kind {
439 // ordered by priority for display (cursor before selection start and after selection end)
440 SelectionEnd,
441 SecondarySelectionEnd,
442 VirtualBlockSelectionEnd,
443 BlockSelectionEnd,
444
445 EmptySelectionStart,
446 EmptySecondarySelectionStart,
447
448 Cursor,
449 VirtualBlockCursor,
450 SecondaryCursor,
451
452 EmptySelectionEnd,
453 EmptySecondarySelectionEnd,
454
455 SelectionStart,
456 SecondarySelectionStart,
457 VirtualBlockSelectionStart,
458 BlockSelectionStart,
459
460 // NewLine is the last item in a line. All other items, including those
461 // with an identical position and a virtual text, must be placed in front.
462 NewLine,
463
464 // only used for output formatting
465 //@{
466 Tab,
467 Backslash,
468 DoubleQuote,
469 //@}
470
471 MaxElement,
472 StartCharacterElement = NewLine,
473 };
474
475 qsizetype pos;
476 Kind kind;
477 int virtualTextLen = 0; // number of virtual characters left
478
479 bool isCharacter() const
480 {
481 return kind >= StartCharacterElement;
482 }
483
484 bool isCursor() const
485 {
486 return kind == Cursor || kind == SecondaryCursor;
487 }
488
489 bool isSelectionStart() const
490 {
491 return kind == SelectionStart || kind == SecondarySelectionStart;
492 }
493
494 bool isSelectionEnd() const
495 {
496 return kind == SelectionEnd || kind == SecondarySelectionEnd;
497 }
498
499 bool isSelection(bool hasVirtualBlockSelection) const
500 {
501 switch (kind) {
502 case SelectionEnd:
503 case SecondarySelectionEnd:
504 case SelectionStart:
505 case SecondarySelectionStart:
506 return true;
507 case VirtualBlockSelectionEnd:
508 case BlockSelectionEnd:
509 case VirtualBlockSelectionStart:
510 case BlockSelectionStart:
511 return hasVirtualBlockSelection;
512 default:
513 return false;
514 }
515 }
516
517 bool isBlockSelectionOrVirtual() const
518 {
519 switch (kind) {
520 case VirtualBlockSelectionEnd:
521 case BlockSelectionEnd:
522 case VirtualBlockCursor:
523 case VirtualBlockSelectionStart:
524 case BlockSelectionStart:
525 return true;
526 default:
527 return false;
528 }
529 }
530
531 bool isEmptySelection() const
532 {
533 switch (kind) {
534 case EmptySelectionEnd:
535 case EmptySecondarySelectionEnd:
536 case EmptySelectionStart:
537 case EmptySecondarySelectionStart:
538 return true;
539 default:
540 return false;
541 }
542 }
543};
544
545ScriptTester::DocumentText::DocumentText() = default;
546ScriptTester::DocumentText::~DocumentText() = default;
547
548/**
549 * Extract item from \p str and add them to \c items.
550 * @param str
551 * @param kind
552 * @param c character in \p str representing a \p kind.
553 * @return number of items extracted
554 */
555std::size_t ScriptTester::DocumentText::addItems(QStringView str, int kind, QChar c)
556{
557 const auto n = items.size();
558
559 qsizetype pos = 0;
560 while (-1 != (pos = str.indexOf(c, pos))) {
561 items.push_back({pos, TextItem::Kind(kind)});
562 ++pos;
563 }
564
565 return items.size() - n;
566}
567
568/**
569 * Extract selection item from \p str and add them to \c items.
570 * Addition is done in pairs by searching for \p start then \p end.
571 * The next \p start starts after the previous \p end.
572 * If \p end is not found, the element is not added.
573 * @param str
574 * @param kind
575 * @param start character in \p str representing a \p kind.
576 * @param end character in \p str representing a \p kind.
577 * @return number of pairs extracted
578 */
579std::size_t ScriptTester::DocumentText::addSelectionItems(QStringView str, int kind, QChar start, QChar end)
580{
581 const auto n = items.size();
582
583 qsizetype pos = 0;
584 while (-1 != (pos = str.indexOf(start, pos))) {
585 qsizetype pos2 = str.indexOf(end, pos + 1);
586 if (pos2 == -1) {
587 break;
588 }
589
590 constexpr int offsetEnd = TextItem::SelectionEnd - TextItem::SelectionStart;
591 static_assert(TextItem::SecondarySelectionStart + offsetEnd == TextItem::SecondarySelectionEnd);
592
593 constexpr int offsetEmptyStart = TextItem::EmptySelectionStart - TextItem::SelectionStart;
594 static_assert(TextItem::SecondarySelectionStart + offsetEmptyStart == TextItem::EmptySecondarySelectionStart);
595
596 constexpr int offsetEmptyEnd = TextItem::EmptySelectionEnd - TextItem::SelectionStart;
597 static_assert(TextItem::SecondarySelectionStart + offsetEmptyEnd == TextItem::EmptySecondarySelectionEnd);
598
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)});
603
604 pos = pos2 + 1;
605 }
606
607 return (items.size() - n) / 2;
608}
609
610/**
611 * Add virtual cursors and selections by deducing them from the primary selection.
612 */
613void ScriptTester::DocumentText::computeBlockSelectionItems()
614{
615 /*
616 * Check if any virtual cursors or selections need to be added.
617 *
618 * Example of possible cases (virtual item represented by @):
619 *
620 * (no item) (2 items) (4 items) (no item) (1 item)
621 * ..[...].. ..[...@.. ..[...@.. ..[.. ..[..
622 * ....... ..@...].. ..@...@.. ..].. ..@..
623 * ....... ....... ..@...].. .... ..]..
624 */
625 if (selection.start().line() == -1 || selection.numberOfLines() <= (selection.columnWidth() ? 0 : 1)) {
626 return;
627 }
628
629 const auto nbLine = selection.numberOfLines();
630 const auto startCursor = selection.start();
631 const auto endCursor = selection.end();
632
633 const auto nbItem = items.size();
634
635 /*
636 * Pre-constructed the number of items that will be added
637 */
638 if (startCursor.column() != endCursor.column()) {
639 items.resize(nbItem + nbLine * 2 + 1);
640 /*
641 * Added NewLine to simplify inserting the last BlockSelectionEnd.
642 * It will be removed at the end.
643 */
644 items[nbItem] = {text.size(), TextItem::NewLine, 0};
645 } else {
646 items.resize(nbItem + nbLine - 1);
647 }
648
649 using Iterator = std::vector<TextItem>::iterator;
650
651 Iterator itemIt = items.begin();
652 Iterator itemEnd = itemIt + nbItem;
653 // skip the inserted NewLine
654 Iterator outIt = itemEnd + (startCursor.column() != endCursor.column());
655
656 auto advanceUntilNewLine = [](Iterator &itemIt) {
657 while (itemIt->kind != TextItem::NewLine) {
658 ++itemIt;
659 }
660 };
661
662 int line = 0;
663 qsizetype textPos = 0;
664
665 /*
666 * Move to start of the selection line
667 */
668 if (startCursor.line() > 0) {
669 for (;; ++itemIt) {
670 advanceUntilNewLine(itemIt);
671 if (++line == startCursor.line()) {
672 textPos = itemIt->pos + 1;
673 ++itemIt;
674 break;
675 }
676 }
677 }
678
679 /**
680 * Advance \p itemIt to \p column, a virtual text or a new line then add the \p kind item.
681 * @return virtual text length of the added item
682 */
683 auto advanceAndPushItem = [&outIt, &textPos](Iterator &itemIt, int column, TextItem::Kind kind) {
684 while (!itemIt->virtualTextLen && itemIt->pos - textPos < column && itemIt->kind != TextItem::NewLine) {
685 ++itemIt;
686 }
687
688 int vlen = 0;
689
690 if (itemIt->pos - textPos >= column) {
691 *outIt = {textPos + column, kind};
692 } else /*if (first->virtualTextLen || first->kind == TextItem::NewLine)*/ {
693 vlen = column - (itemIt->pos - textPos);
694 *outIt = {itemIt->pos, kind, vlen};
695 }
696 ++outIt;
697
698 return vlen;
699 };
700
701 /*
702 * Insert BlockSelectionStart then go to the next line
703 */
704 int vlen = 0;
705 if (startCursor.column() != endCursor.column()) {
706 vlen = advanceAndPushItem(itemIt, endCursor.column(), TextItem::BlockSelectionStart);
707 }
708 advanceUntilNewLine(itemIt);
709 itemIt->virtualTextLen = qMax(itemIt->virtualTextLen, vlen);
710 textPos = itemIt->pos + 1;
711 ++itemIt;
712
713 int leftColumn = startCursor.column();
714 int rightColumn = endCursor.column();
715 if (startCursor.column() > endCursor.column()) {
716 std::swap(leftColumn, rightColumn);
717 }
718
719 /*
720 * Insert VirtualBlockSelection* or VirtualBlockCursor
721 */
722 while (++line < endCursor.line()) {
723 if (leftColumn != rightColumn) {
724 advanceAndPushItem(itemIt, leftColumn, TextItem::VirtualBlockSelectionStart);
725 }
726
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;
731 ++itemIt;
732 }
733
734 /*
735 * Insert BlockSelectionEnd
736 */
737 if (startCursor.column() != endCursor.column()) {
738 int vlen = advanceAndPushItem(itemIt, startCursor.column(), TextItem::BlockSelectionEnd);
739 if (vlen) {
740 advanceUntilNewLine(itemIt);
741 itemIt->virtualTextLen = qMax(itemIt->virtualTextLen, vlen);
742 }
743
744 /*
745 * Remove the new line added
746 */
747 items[nbItem] = items.back();
748 items.pop_back();
749 }
750}
751
752/**
753 * Insert items used only for display with ScriptTester::writeDataTest().
754 */
755void ScriptTester::DocumentText::insertFormattingItems(DocumentTextFormat format)
756{
757 const auto nbItem = items.size();
758
759 if (!hasFormattingItems) {
760 hasFormattingItems = true;
761
762 /*
763 * Insert text replacement items
764 */
765 switch (format) {
766 case DocumentTextFormat::Raw:
767 break;
768 case DocumentTextFormat::EscapeForDoubleQuote:
769 addItems(text, TextItem::Backslash, u'\\');
770 addItems(text, TextItem::DoubleQuote, u'"');
771 [[fallthrough]];
772 case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral:
773 case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder:
774 case DocumentTextFormat::ReplaceTabWithPlaceholder:
775 addItems(text, TextItem::Tab, u'\t');
776 break;
777 }
778 }
779
780 if (blockSelection && !hasBlockSelectionItems) {
781 hasBlockSelectionItems = true;
782 computeBlockSelectionItems();
783 }
784
785 if (nbItem != items.size()) {
786 sortItems();
787 }
788}
789
790/**
791 * Sort items by \c TextItem::pos, then \c TextItem::virtualTextLine, then \c TextItem::kind.
792 */
793void ScriptTester::DocumentText::sortItems()
794{
795 auto cmp = [](const TextItem &a, const TextItem &b) {
796 if (a.pos < b.pos) {
797 return true;
798 }
799 if (a.pos > b.pos) {
800 return false;
801 }
802 if (a.virtualTextLen < b.virtualTextLen) {
803 return true;
804 }
805 if (a.virtualTextLen > b.virtualTextLen) {
806 return false;
807 }
808 return a.kind < b.kind;
809 };
810 std::sort(items.begin(), items.end(), cmp);
811}
812
813/**
814 * Initialize DocumentText with text containing placeholders.
815 * @param input text with placeholders
816 * @param placeholders
817 * @return Error or empty string
818 */
819QString ScriptTester::DocumentText::setText(QStringView input, const Placeholders &placeholders)
820{
821 items.clear();
822 text.clear();
823 secondaryCursors.clear();
824 secondaryCursorsWithSelection.clear();
825 hasFormattingItems = false;
826 hasBlockSelectionItems = false;
827 totalCursor = 0;
828 totalSelection = 0;
829
830 totalLine = 1 + addItems(input, TextItem::NewLine, u'\n');
831
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; \
835 }
836
837 /*
838 * Parse cursor and secondary cursors
839 */
840
841 // add secondary cursors
842 if (placeholders.hasSecondaryCursor()) {
843 totalCursor = addItems(input, TextItem::SecondaryCursor, placeholders.secondaryCursor);
844 RETURN_IF_VIRTUAL_TEXT_CONFLICT(totalCursor, secondaryCursor);
845
846 // when cursor and secondaryCursor have the same placeholder,
847 // the first one found corresponds to the primary cursor
848 if (totalCursor && (!placeholders.hasCursor() || cursorSameAsSecondary(placeholders))) {
849 items[items.size() - totalCursor].kind = TextItem::Cursor;
850 }
851 }
852
853 // add primary cursor when the placeholder is different from the secondary cursor
854 if (placeholders.hasCursor() && (!placeholders.hasSecondaryCursor() || !cursorSameAsSecondary(placeholders))) {
855 const auto nbCursor = addItems(input, TextItem::Cursor, placeholders.cursor);
856 if (nbCursor > 1) {
857 return u"primary cursor set multiple times"_s;
858 }
859 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbCursor, cursor);
860 totalCursor += nbCursor;
861 }
862
863 /*
864 * Parse selection and secondary selections
865 */
866
867 // add secondary selections
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);
872
873 // when selection and secondarySelection have the same placeholders,
874 // the first one found corresponds to the primary selection
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;
878 }
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;
884 }
885 }
886
887 // add primary selection when the placeholders are different from the secondary selection
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;
892 }
893 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbSelection, selectionStart);
894 RETURN_IF_VIRTUAL_TEXT_CONFLICT(nbSelection, selectionEnd);
895 totalSelection += nbSelection;
896 }
897
898#undef RETURN_IF_VIRTUAL_TEXT_CONFLICT
899
900 /*
901 * Search for the first virtual text
902 */
903
904 VirtualText virtualText{-1, -1};
905
906 if (placeholders.hasVirtualText()) {
907 virtualText = findVirtualText(input, 0, placeholders.virtualText);
908 }
909
910 if (virtualText.pos2 != -1 && (totalCursor > 1 || totalSelection > 1)) {
911 return u"virtualText is incompatible with multi-cursor/selection"_s;
912 }
913
914 /*
915 * Update text member, cursor member, selection member,
916 * TextItem::pos and TextItem::virtualTextLen
917 */
918
919 sortItems();
920
921 int line = 0;
922 int charConsumedInPreviousLines = 0;
923 cursor = Cursor::invalid();
924 auto selectionStart = Cursor::invalid();
925 auto selectionEnd = Cursor::invalid();
926 auto secondarySelectionStart = Cursor::invalid();
927 const TextItem *selectionEndItem = nullptr;
928 qsizetype ignoredChars = 0;
929 qsizetype virtualTextLen = 0;
930 qsizetype lastPos = -1;
931
932 /*
933 * Update TextItem::pos and TextItem::virtualTextLen
934 * "abc@@@[@]|\n..."
935 * ~~~ ~ VirtualText
936 * ~ SelectionStart -> update pos=3 and virtualTextLen=3
937 * ~ SelectionEnd -> update pos=3 and virtualTextLen=4
938 * ~ Cursor -> update pos=3 and virtualTextLen=4
939 * ~~ NewLine -> update pos=3 and virtualTextLen=4
940 */
941 for (auto &item : items) {
942 // when the same character is used with several placeholders, the
943 // position does not change and the previous character must not
944 // be ignored because it has not yet been consumed in the input
945 if (lastPos == item.pos) {
946 --ignoredChars;
947 }
948 lastPos = item.pos;
949
950 /*
951 * Update virtual text information
952 */
953
954 // item after virtual text
955 if (virtualText.pos2 != -1 && virtualText.pos2 <= item.pos) {
956 // invalid virtualText input
957 if (item.kind == TextItem::NewLine || virtualText.pos2 != item.pos) {
958 break;
959 }
960 const auto pos = text.size() + ignoredChars;
961 text.append(input.sliced(pos, item.pos - pos - virtualText.size()));
962
963 ignoredChars += virtualText.size();
964 virtualTextLen += virtualText.size();
965 virtualText = findVirtualText(input, virtualText.pos2, placeholders.virtualText);
966 } else if (virtualTextLen) {
967 // text after virtualText but before NewLine
968 if (item.pos != text.size() + ignoredChars) {
969 break;
970 }
971 }
972
973 /*
974 * Update TextItem, cursor and selection
975 */
976
977 item.pos -= ignoredChars;
978 item.virtualTextLen = virtualTextLen;
979
980 auto cursorFromCurrentItem = [&] {
981 return Cursor(line, item.pos - charConsumedInPreviousLines + item.virtualTextLen);
982 };
983
984 switch (item.kind) {
985 case TextItem::Cursor:
986 cursor = cursorFromCurrentItem();
987 break;
988 case TextItem::SelectionStart:
989 case TextItem::EmptySelectionStart:
990 selectionStart = cursorFromCurrentItem();
991 break;
992 case TextItem::SelectionEnd:
993 case TextItem::EmptySelectionEnd:
994 selectionEndItem = &item;
995 selectionEnd = cursorFromCurrentItem();
996 break;
997 case TextItem::SecondaryCursor:
998 secondaryCursors.push_back({cursorFromCurrentItem(), Range::invalid()});
999 break;
1000 case TextItem::SecondarySelectionStart:
1001 case TextItem::EmptySecondarySelectionStart:
1002 secondarySelectionStart = cursorFromCurrentItem();
1003 break;
1004 case TextItem::SecondarySelectionEnd:
1005 case TextItem::EmptySecondarySelectionEnd:
1006 secondaryCursorsWithSelection.push_back({Cursor::invalid(), {secondarySelectionStart, cursorFromCurrentItem()}});
1007 break;
1008 // case TextItem::NewLine:
1009 default:
1010 charConsumedInPreviousLines = item.pos + 1;
1011 virtualTextLen = 0;
1012 ++line;
1013 continue;
1014 }
1015
1016 const auto pos = text.size() + ignoredChars;
1017 const auto len = item.pos + ignoredChars - pos;
1018 text.append(input.sliced(pos, len));
1019 ++ignoredChars;
1020 }
1021
1022 // check for invalid virtual text
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);
1026 }
1027 // check for missing primary selection with secondary selection
1028 if (!secondaryCursorsWithSelection.isEmpty() && selectionStart.line() == -1) {
1029 return u"secondary selections are added without any primary selection"_s;
1030 }
1031 // check for missing primary cursor with secondary cursor
1032 if (!secondaryCursors.empty() && cursor.line() == -1) {
1033 return u"secondary cursors are added without any primary cursor"_s;
1034 }
1035
1036 text += input.sliced(text.size() + ignoredChars);
1037
1038 /*
1039 * The previous loop changes TextItem::pos and the elements must be
1040 * reordered so that the cursor is after an end selection.
1041 * input: `a[b|]c` -> [{1, SelectionStart}, {3, Cursor}, {4, SelectionStop}]
1042 * update indexes: [{1, SelectionStart}, {2, Cursor}, {2, SelectionStop}]
1043 * expected: [{1, SelectionStart}, {2, SelectionStop}, {2, Cursor}]
1044 * -> `a[b]|c`
1045 */
1046 sortItems();
1047
1048 /*
1049 * Check for empty or overlapping selections and for overlapping cursors
1050 */
1051 int countSelection = 0;
1052 qsizetype lastCursorPos = -1;
1053 qsizetype lastSelectionPos = -1;
1054 for (auto &item : items) {
1055 if (item.isSelectionStart()) {
1056 ++countSelection;
1057 if ((countSelection & 1) && lastSelectionPos != item.pos) {
1058 lastSelectionPos = item.pos;
1059 continue;
1060 }
1061 } else if (item.isSelectionEnd()) {
1062 ++countSelection;
1063 if (!(countSelection & 1) && lastSelectionPos != item.pos) {
1064 lastSelectionPos = item.pos;
1065 continue;
1066 }
1067 } else if (item.isCursor()) {
1068 if (countSelection & 1) {
1069 return u"cursor inside a selection"_s;
1070 }
1071 if (lastCursorPos == item.pos) {
1072 return u"one or more cursors overlap"_s;
1073 }
1074 lastCursorPos = item.pos;
1075 continue;
1076 } else if (item.isEmptySelection()) {
1077 if (!(countSelection & 1)) {
1078 continue;
1079 }
1080 } else {
1081 continue;
1082 }
1083 return u"selection %1 is overlapped"_s.arg(countSelection / 2 + 1);
1084 }
1085
1086 /*
1087 * Merge secondaryCursors in secondaryCursorsWithSelection
1088 * and init cursor for secondaryCursorsWithSelection
1089 *
1090 * secondaryCursors = [Cursor{1,3}, Cursor{2,3}, Cursor{3,3}]
1091 * secondaryCursorsWithSelection = [Range{{1,3}, {1,5}}, Range{{3,0}, {3,3}}, Range{{5,0}, {6,0}}]
1092 * => [(Cursor{1,3}, Range{{1,3}, {1,5}}) // merged
1093 * (Cursor{2,3}, Range::invalid()) // inserted
1094 * (Cursor{3,3}, Range{{3,0}, {3,3}}) // merged
1095 * (Cursor{6,0}, Range{{5,0}, {6,0}})] // update
1096 */
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();
1102
1103 // merge
1104 while (it != end && it2 != end2) {
1105 if (it2->range.end() < it->pos) {
1106 it2->pos = it2->range.end();
1107 ++it2;
1108 } else if (it2->range.start() == it->pos || it2->range.end() == it->pos) {
1109 it2->pos = it->pos;
1110 ++it2;
1111 it->pos.setLine(-1);
1112 ++it;
1113 } else {
1114 ++it;
1115 }
1116 }
1117
1118 // update invalid cursor (set to end())
1119 for (; it2 != end2; ++it2) {
1120 it2->pos = it2->range.end();
1121 }
1122
1123 // insert cursor without selection
1124 const auto n = secondaryCursorsWithSelection.size();
1125 for (auto &c : secondaryCursors) {
1126 if (c.pos.line() != -1) {
1127 secondaryCursorsWithSelection.append(c);
1128 }
1129 }
1130 if (n != secondaryCursorsWithSelection.size()) {
1131 std::sort(secondaryCursorsWithSelection.begin(), secondaryCursorsWithSelection.end());
1132 }
1133 } else if (!secondaryCursorsWithSelection.isEmpty()) {
1134 for (auto &c : secondaryCursorsWithSelection) {
1135 c.pos = c.range.end();
1136 }
1137 } else {
1138 secondaryCursorsWithSelection.assign(secondaryCursors.begin(), secondaryCursors.end());
1139 }
1140
1141 /*
1142 * Init cursor when no specified
1143 */
1144 if (cursor.line() == -1) {
1145 if (selectionEndItem) {
1146 // add cursor to end of selection
1147 auto afterSelection = items.begin() + (selectionEndItem - items.data()) + 1;
1148 items.insert(afterSelection, {selectionEndItem->pos, TextItem::Cursor, selectionEndItem->virtualTextLen});
1149 cursor = selectionEnd;
1150 } else {
1151 // add cursor to end of document
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);
1155 }
1156 }
1157
1158 selection = {selectionStart, selectionEnd};
1159
1160 // check that the cursor is on a selection if one exists
1161 if (selection.start().line() != -1 && !selection.boundaryAtCursor(cursor)) {
1162 return u"the cursor is not at the limit of the selection"_s;
1163 }
1164
1165 return QString();
1166}
1167
1168ScriptTester::ScriptTester(QIODevice *output,
1169 const Format &format,
1170 const Paths &paths,
1171 const TestExecutionConfig &executionConfig,
1172 const DiffCommand &diffCmd,
1173 Placeholders placeholders,
1174 QJSEngine *engine,
1175 DocumentPrivate *doc,
1176 ViewPrivate *view,
1177 QObject *parent)
1178 : QObject(parent)
1179 , m_engine(engine)
1180 , m_doc(doc)
1181 , m_view(view)
1182 , m_fallbackPlaceholders(format.fallbackPlaceholders)
1183 , m_defaultPlaceholders(placeholders)
1184 , m_placeholders(placeholders)
1185 , m_editorConfig(makeEditorConfig())
1186 , m_stream(output)
1187 , m_format(format)
1188 , m_paths(paths)
1189 , m_executionConfig(executionConfig)
1190 , m_diffCmd(diffCmd)
1191{
1192 // starts a config without ever finishing it: no need to update anything
1193 auto *docConfig = m_doc->config();
1194 docConfig->configStart();
1195 docConfig->setIndentPastedText(true);
1196}
1197
1198QString ScriptTester::read(const QString &name)
1199{
1200 // the error will also be written to this variable,
1201 // but ignored because QJSEngine will then throw an exception
1202 QString contentOrError;
1203
1204 QString fullName = getPath(m_engine, name, m_paths.scripts, &contentOrError);
1205 if (!fullName.isEmpty()) {
1206 readFile(m_engine, fullName, &contentOrError, &contentOrError);
1207 }
1208 return contentOrError;
1209}
1210
1211void ScriptTester::require(const QString &name)
1212{
1213 // check include guard
1214 auto it = m_libraryFiles.find(name);
1215 if (it != m_libraryFiles.end()) {
1216 // re-throw previous exception
1217 if (!it->isEmpty()) {
1218 m_engine->throwError(QJSValue::URIError, *it);
1219 }
1220 return;
1221 }
1222
1223 it = m_libraryFiles.insert(name, QString());
1224
1225 QString fullName = getPath(m_engine, name, m_paths.libraries, &*it);
1226 if (fullName.isEmpty()) {
1227 return;
1228 }
1229
1230 QString program;
1231 if (!readFile(m_engine, fullName, &program, &*it)) {
1232 return;
1233 }
1234
1235 // eval in current script engine
1236 const QJSValue val = m_engine->evaluate(program, fullName);
1237 if (!val.isError()) {
1238 return;
1239 }
1240
1241 // propagate exception
1242 *it = val.toString();
1243 m_engine->throwError(val);
1244}
1245
1246void ScriptTester::debug(const QString &message)
1247{
1248 const auto requireStack = m_format.debugOptions.testAnyFlags(DebugOption::WriteStackTrace | DebugOption::WriteFunction);
1249 const auto err = m_format.debugOptions ? generateStack(m_engine) : QJSValue();
1250 const auto stack = requireStack ? err.toString() : QString();
1251
1252 /*
1253 * Display format:
1254 *
1255 * {fileName}:{lineNumber}: {funcName}: DEBUG: {msg}
1256 * ~~~~~~~~~~~~~~~~~~~~~~~~~ WriteLocation option
1257 * ~~~~~~~~~~~~ WriteFunction option
1258 * {stackTrace} WriteStackTrace option
1259 */
1260
1261 // add {fileName}:{lineNumber}:
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;
1266 };
1267 const auto fileName = err.property(u"fileName"_s);
1268 // qrc file has no fileName
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);
1273 } else {
1274 pushLocation(fileName.toString(), err.property(u"lineNumber"_s).toString());
1275 }
1276 }
1277
1278 // add {funcName}:
1279 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteFunction)) {
1280 const QStringView stackView = stack;
1281 const qsizetype pos = stackView.indexOf('@'_L1);
1282 if (pos > 0) {
1283 m_debugMsg += m_format.colors.program % stackView.first(pos) % m_format.colors.reset % m_format.colors.debugMsg % ": "_L1 % m_format.colors.reset;
1284 }
1285 }
1286
1287 // add DEBUG: {msg}
1288 m_debugMsg +=
1289 m_format.colors.debugMarker % u"DEBUG:"_sv % m_format.colors.reset % m_format.colors.debugMsg % u' ' % message % m_format.colors.reset % u'\n';
1290
1291 // add {stackTrace}
1292 if (m_format.debugOptions.testAnyFlag(DebugOption::WriteStackTrace)) {
1293 pushException(m_debugMsg, m_format.colors, stack, u"| "_sv);
1294 }
1295
1296 // flush
1297 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1298 if (!m_hasDebugMessage && m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation)) {
1299 m_stream << '\n';
1300 }
1301 m_stream << m_debugMsg;
1302 m_stream.flush();
1303 m_debugMsg.clear();
1304 }
1305
1306 m_hasDebugMessage = true;
1307}
1308
1309void ScriptTester::print(const QString &message)
1310{
1311 if (m_format.debugOptions.testAnyFlags(DebugOption::WriteLocation | DebugOption::WriteFunction)) {
1312 /*
1313 * Display format:
1314 *
1315 * {fileName}:{lineNumber}: {funcName}: PRINT: {msg}
1316 * ~~~~~~~~~~~~~~~~~~~~~~~~~ WriteLocation option
1317 * ~~~~~~~~~~~~ WriteFunction option
1318 */
1319
1320 const auto errStr = generateStack(m_engine).toString();
1321 QStringView err = errStr;
1322 auto nl = err.indexOf(u'\n');
1323 if (nl != -1) {
1324 auto stackLine = parseStackLine(err.sliced(nl + 1));
1325
1326 // add {fileName}:{lineNumber}:
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;
1330 }
1331
1332 // add {funcName}:
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;
1336 }
1337 }
1338 }
1339
1340 // add PRINT: {msg}
1341 m_stream << m_format.colors.debugMarker << "PRINT:"_L1 << m_format.colors.reset << m_format.colors.debugMsg << ' ' << message << m_format.colors.reset
1342 << '\n';
1343 m_stream.flush();
1344}
1345
1346QJSValue ScriptTester::loadModule(const QString &fileName)
1347{
1348 QString error;
1349 const auto path = getModulePath(m_engine, fileName, m_paths.modules, &error);
1350 if (path.isEmpty()) {
1351 return QJSValue();
1352 }
1353
1354 auto mod = m_engine->importModule(path);
1355 if (mod.isError()) {
1356 m_engine->throwError(mod);
1357 }
1358 return mod;
1359}
1360
1361void ScriptTester::loadScript(const QString &fileName)
1362{
1363 QString contentOrError;
1364 const auto path = getModulePath(m_engine, fileName, m_paths.scripts, &contentOrError);
1365 if (path.isEmpty()) {
1366 return;
1367 }
1368
1369 if (!readFile(m_engine, path, &contentOrError, &contentOrError)) {
1370 return;
1371 }
1372
1373 // eval in current script engine
1374 const QJSValue val = m_engine->evaluate(contentOrError, fileName);
1375 if (!val.isError()) {
1376 return;
1377 }
1378
1379 // propagate exception
1380 m_engine->throwError(val);
1381}
1382
1383bool ScriptTester::startTestCase(const QString &name, int nthStack)
1384{
1385 if (m_executionConfig.patternType == PatternType::Inactive) {
1386 return true;
1387 }
1388
1389 const bool hasMatch = m_executionConfig.pattern.matchView(name).hasMatch();
1390 const bool exclude = m_executionConfig.patternType == PatternType::Exclude;
1391 if (exclude != hasMatch) {
1392 return true;
1393 }
1394
1395 ++m_skipedCounter;
1396
1397 /*
1398 * format with optional testName
1399 * ${fileName}:${lineNumber}: ${testName}: SKIP
1400 */
1401
1402 // ${fileName}:${lineNumber}:
1403 writeLocation(nthStack);
1404 // ${testName}: SKIP
1405 m_stream << m_format.colors.testName << name << m_format.colors.reset << ": "_L1 << m_format.colors.labelInfo << "SKIP"_L1 << m_format.colors.reset << '\n';
1406
1407 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1408 m_stream.flush();
1409 }
1410
1411 return false;
1412}
1413
1414void ScriptTester::setConfig(const QJSValue &config)
1415{
1416 bool updateConf = false;
1417
1418#define READ_CONFIG(fn, name) \
1419 fn(config, u"" #name ""_s, [&](auto value) { \
1420 m_editorConfig.name = std::move(value); \
1421 updateConf = true; \
1422 })
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);
1429#undef READ_CONFIG
1430
1431 if (updateConf) {
1432 m_editorConfig.updated = false;
1433 m_editorConfig.inherited = m_configStack.empty();
1434 }
1435
1436#define READ_PLACEHOLDER(name) \
1437 readString(config, u"" #name ""_s, [&](QString s) { \
1438 m_placeholders.name = s.isEmpty() ? u'\0' : s[0]; \
1439 }); \
1440 readString(config, u"" #name "2"_s, [&](QString s) { \
1441 m_fallbackPlaceholders.name = s.isEmpty() ? m_format.fallbackPlaceholders.name : s[0]; \
1442 })
1443 READ_PLACEHOLDER(cursor);
1444 READ_PLACEHOLDER(secondaryCursor);
1445 READ_PLACEHOLDER(virtualText);
1446#undef READ_PLACEHOLDER
1447
1448 auto readSelection = [&](QString name, QString fallbackName, QChar Placeholders::*startMem, QChar Placeholders::*endMem) {
1449 readString(config, name, [&](QString s) {
1450 switch (s.size()) {
1451 case 0:
1452 m_placeholders.*startMem = m_placeholders.*endMem = u'\0';
1453 break;
1454 case 1:
1455 m_placeholders.*startMem = m_placeholders.*endMem = s[0];
1456 break;
1457 default:
1458 m_placeholders.*startMem = s[0];
1459 m_placeholders.*endMem = s[1];
1460 break;
1461 }
1462 });
1463 readString(config, fallbackName, [&](QString s) {
1464 switch (s.size()) {
1465 case 0:
1466 m_fallbackPlaceholders.*startMem = m_format.fallbackPlaceholders.*startMem;
1467 m_fallbackPlaceholders.*endMem = m_format.fallbackPlaceholders.*endMem;
1468 break;
1469 case 1:
1470 m_fallbackPlaceholders.*startMem = m_fallbackPlaceholders.*endMem = s[0];
1471 break;
1472 default:
1473 m_fallbackPlaceholders.*startMem = s[0];
1474 m_fallbackPlaceholders.*endMem = s[1];
1475 break;
1476 }
1477 });
1478 };
1479 readSelection(u"selection"_s, u"selection2"_s, &Placeholders::selectionStart, &Placeholders::selectionEnd);
1480 readSelection(u"secondarySelection"_s, u"secondarySelection2"_s, &Placeholders::secondarySelectionStart, &Placeholders::secondarySelectionEnd);
1481}
1482
1483void ScriptTester::resetConfig()
1484{
1485 m_fallbackPlaceholders = m_format.fallbackPlaceholders;
1486 m_placeholders = m_defaultPlaceholders;
1487 m_editorConfig = makeEditorConfig();
1488 m_configStack.clear();
1489}
1490
1491void ScriptTester::pushConfig()
1492{
1493 m_configStack.emplace_back(Config{m_fallbackPlaceholders, m_placeholders, m_editorConfig});
1494 m_editorConfig.inherited = true;
1495}
1496
1497void ScriptTester::popConfig()
1498{
1499 if (m_configStack.empty()) {
1500 return;
1501 }
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();
1509}
1510
1511QJSValue ScriptTester::evaluate(const QString &program)
1512{
1513 QStringList stack;
1514 auto err = m_engine->evaluate(program, u"(program)"_s, 1, &stack);
1515 if (!stack.isEmpty()) {
1516 m_engine->throwError(err);
1517 }
1518 return err;
1519}
1520
1521void ScriptTester::setInput(const QString &input, bool blockSelection)
1522{
1523 auto err = m_input.setText(input, m_placeholders);
1524 if (err.isEmpty() && checkMultiCursorCompatibility(m_input, blockSelection, &err)) {
1525 m_input.blockSelection = blockSelection;
1526 initInputDoc();
1527 } else {
1528 m_engine->throwError(err);
1529 ++m_errorCounter;
1530 }
1531}
1532
1533void ScriptTester::moveExpectedOutputToInput(bool blockSelection)
1534{
1535 // prefer swap to std::move to avoid freeing vector / list memory
1536 std::swap(m_input, m_expected);
1537 reuseInput(blockSelection);
1538}
1539
1540void ScriptTester::reuseInput(bool blockSelection)
1541{
1542 QString err;
1543 if (checkMultiCursorCompatibility(m_input, blockSelection, &err)) {
1544 m_input.blockSelection = blockSelection;
1545 initInputDoc();
1546 } else {
1547 m_engine->throwError(err);
1548 ++m_errorCounter;
1549 }
1550}
1551
1552bool ScriptTester::reuseInputWithBlockSelection()
1553{
1554 QString err;
1555 if (!checkMultiCursorCompatibility(m_input, true, &err)) {
1556 return false;
1557 }
1558 m_input.blockSelection = true;
1559 initInputDoc();
1560 return true;
1561}
1562
1563bool ScriptTester::checkMultiCursorCompatibility(const DocumentText &doc, bool blockSelection, QString *err)
1564{
1565 if (doc.totalSelection > 1 || doc.totalCursor > 1) {
1566 if (blockSelection) {
1567 *err = u"blockSelection is incompatible with multi-cursor/selection"_s;
1568 return false;
1569 }
1570 if (m_doc->config()->ovr()) {
1571 *err = u"overrideMode is incompatible with multi-cursor/selection"_s;
1572 return false;
1573 }
1574 }
1575
1576 return true;
1577}
1578
1579void ScriptTester::initDocConfig()
1580{
1581 if (m_editorConfig.updated) {
1582 return;
1583 }
1584
1585 m_editorConfig.updated = true;
1586
1587 m_view->config()->setValue(KateViewConfig::AutoBrackets, m_editorConfig.autoBrackets);
1588
1589 m_doc->setHighlightingMode(m_editorConfig.syntax);
1590
1591 auto *docConfig = m_doc->config();
1592 // docConfig->configStart();
1593 docConfig->setIndentationMode(m_editorConfig.indentationMode);
1594 docConfig->setIndentationWidth(m_editorConfig.indentationWidth);
1595 docConfig->setReplaceTabsDyn(m_editorConfig.replaceTabs);
1596 docConfig->setTabWidth(m_editorConfig.tabWidth);
1597 // docConfig->configEnd();
1598
1599 syncIndenter();
1600}
1601
1602void ScriptTester::syncIndenter()
1603{
1604 // faster to remove then put the view
1605 m_doc->removeView(m_view);
1606 m_doc->updateConfig(); // synchronize indenter
1607 m_doc->addView(m_view);
1608}
1609
1610void ScriptTester::initInputDoc()
1611{
1612 initDocConfig();
1613
1614 m_doc->setText(m_input.text);
1615
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);
1620
1621 if (!m_input.secondaryCursorsWithSelection.isEmpty()) {
1622 m_view->addSecondaryCursorsWithSelection(m_input.secondaryCursorsWithSelection);
1623 }
1624}
1625
1626void ScriptTester::setExpectedOutput(const QString &expected, bool blockSelection)
1627{
1628 auto err = m_expected.setText(expected, m_placeholders);
1629 if (err.isEmpty() && checkMultiCursorCompatibility(m_expected, blockSelection, &err)) {
1630 m_expected.blockSelection = blockSelection;
1631 } else {
1632 m_engine->throwError(err);
1633 ++m_errorCounter;
1634 }
1635}
1636
1637void ScriptTester::reuseExpectedOutput(bool blockSelection)
1638{
1639 QString err;
1640 if (checkMultiCursorCompatibility(m_expected, blockSelection, &err)) {
1641 m_expected.blockSelection = blockSelection;
1642 } else {
1643 m_engine->throwError(err);
1644 ++m_errorCounter;
1645 }
1646}
1647
1648void ScriptTester::copyInputToExpectedOutput(bool blockSelection)
1649{
1650 m_expected = m_input;
1651 reuseExpectedOutput(blockSelection);
1652}
1653
1654bool ScriptTester::checkOutput()
1655{
1656 /*
1657 * Init m_output
1658 */
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();
1664
1665 // init secondaryCursors
1666 {
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) {
1671 *it++ = {
1672 .pos = c.cursor(),
1673 .range = c.range ? c.range->toRange() : Range::invalid(),
1674 };
1675 }
1676 }
1677
1678 /*
1679 * Check output
1680 */
1681 if (m_output.text != m_expected.text || m_output.blockSelection != m_expected.blockSelection) {
1682 // differ
1683 } else if (!m_expected.blockSelection) {
1684 // compare ignoring virtual column
1685 auto cursorEq = [this](const Cursor &output, const Cursor &expected) {
1686 if (output.line() != expected.line()) {
1687 return false;
1688 }
1689 int lineLen = m_doc->lineLength(expected.line());
1690 int column = qMin(lineLen, expected.column());
1691 return output.column() == column;
1692 };
1693 auto rangeEq = [=](const Range &output, const Range &expected) {
1694 return cursorEq(output.start(), expected.start()) && cursorEq(output.end(), expected.end());
1695 };
1696 auto SecondaryEq = [=](const ViewPrivate::PlainSecondaryCursor &c1, const ViewPrivate::PlainSecondaryCursor &c2) {
1697 if (!cursorEq(c1.pos, c2.pos) || c1.range.isValid() != c2.range.isValid()) {
1698 return false;
1699 }
1700 return !c1.range.isValid() || rangeEq(c1.range, c2.range);
1701 };
1702
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(),
1708 SecondaryEq)) {
1709 return true;
1710 }
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;
1718 })) {
1719 return true;
1720 }
1721
1722 /*
1723 * Create a list of all cursors in the document sorted by position
1724 * with their associated type (TextItem::Kind)
1725 */
1726
1727 struct CursorItem {
1728 Cursor cursor;
1729 TextItem::Kind kind;
1730 };
1732 cursorItems.resize(m_output.secondaryCursors.size() * 3 + 3);
1733
1734 auto it = cursorItems.begin();
1735 if (m_output.cursor.isValid()) {
1736 *it++ = {m_output.cursor, TextItem::Cursor};
1737 }
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};
1744 }
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};
1753 }
1754 }
1755
1756 const auto end = it;
1757 std::sort(cursorItems.begin(), end, [](const CursorItem &a, const CursorItem &b) {
1758 if (a.cursor < b.cursor) {
1759 return true;
1760 }
1761 if (a.cursor > b.cursor) {
1762 return false;
1763 }
1764 return a.kind < b.kind;
1765 });
1766 it = cursorItems.begin();
1767
1768 /*
1769 * Init m_output.items
1770 */
1771
1772 QStringView output = m_output.text;
1773 m_output.items.clear();
1774 m_output.hasFormattingItems = false;
1775 m_output.hasBlockSelectionItems = false;
1776
1777 qsizetype line = 0;
1778 qsizetype pos = 0;
1779 for (;;) {
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});
1786 }
1787 if (nextPos == -1) {
1788 break;
1789 }
1790 m_output.items.push_back({nextPos, TextItem::NewLine, virtualTextLen});
1791 pos = nextPos + 1;
1792 ++line;
1793 }
1794
1795 // no sorting, items are inserted in the right order
1796 // m_output.sortItems();
1797
1798 return false;
1799}
1800
1801bool ScriptTester::incrementCounter(bool isSuccessNotAFailure, bool xcheck)
1802{
1803 if (!xcheck) {
1804 m_successCounter += isSuccessNotAFailure;
1805 m_failureCounter += !isSuccessNotAFailure;
1806 return isSuccessNotAFailure;
1807 } else if (m_executionConfig.xCheckAsFailure) {
1808 m_failureCounter++;
1809 return false;
1810 } else {
1811 m_xSuccessCounter += !isSuccessNotAFailure;
1812 m_xFailureCounter += isSuccessNotAFailure;
1813 return !isSuccessNotAFailure;
1814 }
1815}
1816
1817void ScriptTester::incrementError()
1818{
1819 ++m_errorCounter;
1820}
1821
1822void ScriptTester::incrementBreakOnError()
1823{
1824 ++m_breakOnErrorCounter;
1825}
1826
1827int ScriptTester::countError() const
1828{
1829 return m_errorCounter + m_failureCounter + m_xFailureCounter;
1830}
1831
1832bool ScriptTester::hasTooManyErrors() const
1833{
1834 return m_executionConfig.maxError > 0 && countError() >= m_executionConfig.maxError;
1835}
1836
1837int ScriptTester::startTest()
1838{
1839 m_debugMsg.clear();
1840 m_hasDebugMessage = false;
1841 int flags = 0;
1842 flags |= m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput) ? 1 : 0;
1843 flags |= m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation) ? 2 : 0;
1844 return flags;
1845}
1846
1847void ScriptTester::endTest(bool ok, bool showBlockSelection)
1848{
1849 if (!ok) {
1850 return;
1851 }
1852
1853 constexpr auto mask = TestFormatOptions() | TestFormatOption::AlwaysWriteLocation | TestFormatOption::AlwaysWriteInputOutput;
1854 if ((m_format.testFormatOptions & mask) != TestFormatOption::AlwaysWriteLocation) {
1855 return;
1856 }
1857
1858 if (showBlockSelection) {
1859 m_stream << m_format.colors.blockSelectionInfo << (m_input.blockSelection ? " [blockSelection=1]"_L1 : " [blockSelection=0]"_L1)
1860 << m_format.colors.reset;
1861 }
1862 m_stream << m_format.colors.success << " Ok\n"_L1 << m_format.colors.reset;
1863}
1864
1865void ScriptTester::writeTestExpression(const QString &name, const QString &type, int nthStack, const QString &program)
1866{
1867 /*
1868 * format with optional testName
1869 * ${fileName}:${lineNumber}: ${testName}: ${type} `${program}`
1870 */
1871
1872 // ${fileName}:${lineNumber}:
1873 writeLocation(nthStack);
1874 // ${testName}:
1875 writeTestName(name);
1876 // ${type} `${program}`
1877 writeTypeAndProgram(type, program);
1878
1879 m_stream << m_format.colors.reset;
1880
1881 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
1882 m_stream.flush();
1883 }
1884}
1885
1886void ScriptTester::writeDualModeAborted(const QString &name, int nthStack)
1887{
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;
1893}
1894
1895void ScriptTester::writeTestName(const QString &name)
1896{
1897 if (!m_format.testFormatOptions.testAnyFlag(TestFormatOption::HiddenTestName) && !name.isEmpty()) {
1898 m_stream << m_format.colors.testName << name << m_format.colors.reset << ": "_L1;
1899 }
1900}
1901
1902void ScriptTester::writeTypeAndProgram(const QString &type, const QString &program)
1903{
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;
1906}
1907
1908void ScriptTester::writeTestResult(const QString &name,
1909 const QString &type,
1910 int nthStack,
1911 const QString &program,
1912 const QString &msg,
1913 const QJSValue &exception,
1914 const QString &result,
1915 const QString &expectedResult,
1916 int options)
1917{
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;
1925
1926 const bool alwaysWriteTest = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation);
1927 const bool alwaysWriteInputOutput = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput);
1928
1929 const bool outputDiffer = !(options & (outputIsOk | ignoreInputOutput));
1930 const bool resultDiffer = (options & expectedNoErrorButError) || ((options & containsResultOrError) && !(options & sameResultOrError));
1931
1932 /*
1933 * format with optional testName and msg
1934 * alwaysWriteTest = false
1935 * ${fileName}:${lineNumber}: ${testName}: {Output/Result} differs
1936 * ${type} `${program}` -- ${msg} ${blockSelectionMode}:
1937 *
1938 * alwaysWriteTest = true
1939 * format with optional msg
1940 * {Output/Result} differs -- ${msg} ${blockSelectionMode}:
1941 */
1942 if (alwaysWriteTest) {
1943 if (alwaysWriteInputOutput && !outputDiffer && !resultDiffer) {
1944 m_stream << m_format.colors.success << " OK"_L1;
1945 } else if (!m_hasDebugMessage) {
1946 m_stream << '\n';
1947 }
1948 } else {
1949 // ${fileName}:${lineNumber}:
1950 writeLocation(nthStack);
1951 // ${testName}:
1952 writeTestName(name);
1953 }
1954 // {Output/Result} differs
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;
1963 }
1964 if (!alwaysWriteTest) {
1965 m_stream << '\n';
1966 // ${type} `${program}`
1967 writeTypeAndProgram(type, program);
1968 }
1969 // -- ${msg}
1970 if (!msg.isEmpty()) {
1971 if (!alwaysWriteTest) {
1972 m_stream << m_format.colors.error;
1973 }
1974 m_stream << " -- "_L1 << msg << m_format.colors.reset;
1975 } else if (alwaysWriteTest) {
1976 m_stream << m_format.colors.reset;
1977 }
1978 // ${blockSelectionMode}:
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);
1982 } else {
1983 m_stream << " [blockSelection=(input="_L1 << m_input.blockSelection << ", output="_L1 << m_output.blockSelection << ", expected="_L1
1984 << m_expected.blockSelection << ")]"_L1;
1985 }
1986 m_stream << m_format.colors.reset << ":\n"_L1;
1987
1988 /*
1989 * Display buffered debug messages
1990 */
1991 m_stream << m_debugMsg;
1992 m_debugMsg.clear();
1993
1994 /*
1995 * Editor result block
1996 */
1997 if (!(options & ignoreInputOutput)) {
1998 writeDataTest(options & outputIsOk);
1999 }
2000
2001 /*
2002 * Function result block (exception caught or return value)
2003 */
2004 if (options & (containsResultOrError | sameResultOrError)) {
2005 if (!(options & ignoreInputOutput)) {
2006 m_stream << " ---------\n"_L1;
2007 }
2008
2009 // display
2010 // result: ... (optional)
2011 // expected: ...
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';
2014 if (!result.isEmpty()) {
2015 writeLabel(m_stream, m_format.colors, false, " result: "_L1);
2016 m_stream << m_format.colors.result << result << m_format.colors.reset << '\n';
2017 }
2018 m_stream << " expected: "_L1;
2019 m_stream << m_format.colors.result << expectedResult << m_format.colors.reset << '\n';
2020 }
2021 // display
2022 // result: ... (or error:)
2023 // expected: ... (optional)
2024 else {
2025 auto label = (options & (isResultNotError | sameResultOrError)) ? " result: "_L1 : " error: "_L1;
2026 writeLabel(m_stream, m_format.colors, options & sameResultOrError, label);
2027
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);
2034 }
2035 }
2036 }
2037
2038 /*
2039 * Uncaught exception block
2040 */
2041 if (options & expectedNoErrorButError) {
2042 m_stream << " ---------\n"_L1 << m_format.colors.error << " Uncaught exception: "_L1 << exception.toString() << '\n';
2043 writeException(exception, u" | "_sv);
2044 }
2045
2046 m_stream << '\n';
2047}
2048
2049void ScriptTester::writeException(const QJSValue &exception, QStringView prefix)
2050{
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;
2054 } else {
2055 m_stringBuffer.clear();
2056 pushException(m_stringBuffer, m_format.colors, stack.toString(), prefix);
2057 m_stream << m_stringBuffer;
2058 }
2059}
2060
2061void ScriptTester::writeLocation(int nthStack)
2062{
2063 const auto errStr = generateStack(m_engine).toString();
2064 const QStringView err = errStr;
2065
2066 // skip lines
2067 qsizetype startIndex = 0;
2068 while (nthStack-- > 0) {
2069 qsizetype pos = err.indexOf('\n'_L1, startIndex);
2070 if (pos <= -1) {
2071 break;
2072 }
2073 startIndex = pos + 1;
2074 }
2075
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;
2079}
2080
2081/**
2082 * Builds the map to convert \c TextItem::Kind to \c QStringView.
2083 */
2084struct ScriptTester::Replacements {
2085 const QChar selectionPlaceholders[2];
2086 const QChar secondarySelectionPlaceholders[2];
2087 const QChar virtualTextPlaceholder;
2088
2089 // index 0 = color; index 1 = replacement text
2090 QStringView replacements[TextItem::MaxElement][2];
2091 int tabWidth = 0;
2092
2093 static constexpr int tabBufferLen = 16;
2094 QChar tabBuffer[tabBufferLen];
2095
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)
2098
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])
2105 {
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;
2110 // ignore Empty(Secondary)SelectionEnd
2111
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};
2116
2117 replacements[TextItem::Cursor][0] = colors.cursor;
2118 replacements[TextItem::Cursor][1] =
2119 GET_PLACEHOLDER(cursor, selectionPlaceholders[0] != placeholders.cursor && selectionPlaceholders[1] != placeholders.cursor);
2120
2121 replacements[TextItem::SecondaryCursor][0] = colors.secondaryCursor;
2122 replacements[TextItem::SecondaryCursor][1] = GET_PLACEHOLDER(
2123 secondaryCursor,
2124 selectionPlaceholders[0] != placeholders.secondaryCursor && selectionPlaceholders[1] != placeholders.secondaryCursor
2125 && secondarySelectionPlaceholders[0] != placeholders.secondaryCursor && secondarySelectionPlaceholders[1] != placeholders.secondaryCursor);
2126
2127 replacements[TextItem::SelectionStart][0] = colors.selection;
2128 replacements[TextItem::SelectionEnd][0] = colors.selection;
2129
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;
2135 }
2136
2137#undef GET_CH_PLACEHOLDER
2138#undef GET_PLACEHOLDER
2139
2140 void initEscapeForDoubleQuote(const Colors &colors)
2141 {
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;
2150 }
2151
2152 void initReplaceNewLineAndTabWithLiteral(const Colors &colors)
2153 {
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;
2158 }
2159
2160 void initNewLine(const Format &format)
2161 {
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;
2165 }
2166
2167 void initTab(const Format &format, DocumentPrivate *doc)
2168 {
2169 const auto &repl = format.textReplacement;
2170 tabWidth = qMin(doc->config()->tabWidth(), tabBufferLen);
2171 if (tabWidth > 0) {
2172 for (int i = 0; i < tabWidth - 1; ++i) {
2173 tabBuffer[i] = repl.tab1;
2174 }
2175 tabBuffer[tabWidth - 1] = repl.tab2;
2176 }
2177 replacements[TextItem::Tab][0] = format.colors.resultReplacement;
2178 replacements[TextItem::Tab][1] = QStringView(tabBuffer, tabWidth);
2179 }
2180
2181 void initSelections(bool hasVirtualBlockSelection, bool reverseSelection)
2182 {
2183 if (hasVirtualBlockSelection && reverseSelection) {
2184 replacements[TextItem::SelectionStart][1] = {&selectionPlaceholders[1], 1};
2185 replacements[TextItem::SelectionEnd][1] = {&selectionPlaceholders[0], 1};
2186
2187 replacements[TextItem::BlockSelectionStart][1] = {&selectionPlaceholders[0], 1};
2188 replacements[TextItem::BlockSelectionEnd][1] = {&selectionPlaceholders[1], 1};
2189 } else {
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};
2195 }
2196 }
2197
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};
2202 } else {
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();
2208 }
2209 }
2210
2211 const QStringView (&operator[](TextItem::Kind i) const)[2]
2212 {
2213 return replacements[i];
2214 }
2215};
2216
2217void ScriptTester::writeDataTest(bool outputIsOk)
2218{
2219 Replacements replacements(m_format.colors, m_placeholders, m_fallbackPlaceholders);
2220
2221 const auto textFormat = (m_input.blockSelection || m_output.blockSelection || (outputIsOk && m_expected.blockSelection))
2222 ? m_format.documentTextFormatWithBlockSelection
2223 : m_format.documentTextFormat;
2224
2225 bool alignNL = true;
2226
2227 switch (textFormat) {
2228 case DocumentTextFormat::Raw:
2229 break;
2230 case DocumentTextFormat::EscapeForDoubleQuote:
2231 replacements.initEscapeForDoubleQuote(m_format.colors);
2232 alignNL = false;
2233 break;
2234 case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral:
2235 replacements.initReplaceNewLineAndTabWithLiteral(m_format.colors);
2236 alignNL = false;
2237 break;
2238 case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder:
2239 replacements.initNewLine(m_format);
2240 [[fallthrough]];
2241 case DocumentTextFormat::ReplaceTabWithPlaceholder:
2242 replacements.initTab(m_format, m_doc);
2243 break;
2244 }
2245
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);
2248
2249 const QStringView inSelectionFormat = (hasVirtualBlockSelection && docText.selection.columnWidth() == 0) ? QStringView() : m_format.colors.inSelection;
2250
2251 replacements.initSelections(hasVirtualBlockSelection, docText.selection.columnWidth() < 0);
2252
2253 QStringView inSelection;
2254 bool showCarret = (carretColumn != -1);
2255 qsizetype line = 0;
2256 qsizetype previousLinePos = 0;
2257 qsizetype virtualTabLen = 0;
2258 qsizetype textPos = 0;
2259 qsizetype virtualTextLen = 0;
2260 for (const TextItem &item : docText.items) {
2261 // displays the text between 2 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;
2265 }
2266
2267 // insert virtual text symbols
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);
2272 m_stream << ""_L1;
2273 m_stream.setFieldWidth(0);
2274 if (!m_format.colors.virtualText.isEmpty() || !inSelection.isEmpty()) {
2275 m_stream << m_format.colors.reset;
2276 }
2277 textPos = item.pos + item.isCharacter();
2278 virtualTextLen = item.virtualTextLen;
2279 }
2280
2281 // update selection text state (close selection)
2282 const bool isInSelection = !inSelection.isEmpty();
2283 if (isInSelection && item.isSelection(hasVirtualBlockSelection)) {
2284 inSelection = QStringView();
2285 }
2286
2287 // display item
2288 const auto &replacement = replacements[item.kind];
2289 if (!replacement[1].isEmpty()) {
2290 m_stream << replacement[0] << inSelection;
2291 // adapts tab size to be a multiple of tabWidth
2292 // tab="->" tabWidth=4
2293 // input: ab\t\tc
2294 // output: ab->--->c
2295 // ~~~~ = tabWidth
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);
2301 } else {
2302 m_stream << replacement[1];
2303 }
2304 }
2305
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) {
2310 m_stream << '\n';
2311 if (showCarret && carretLine == line) {
2312 showCarret = false;
2313 writeCarretLine(m_stream, m_format.colors, carretColumn);
2314 }
2315 m_stream << " "_L1;
2316 ++line;
2317 }
2318 }
2319 if (item.kind == TextItem::NewLine) {
2320 virtualTabLen = 0;
2321 virtualTextLen = 0;
2322 previousLinePos = item.pos + 1;
2323 }
2324
2325 // update selection text state (open selection)
2326 if (!isInSelection && item.isSelection(hasVirtualBlockSelection)) {
2327 inSelection = inSelectionFormat;
2328 }
2329
2330 textPos = item.pos + item.isCharacter();
2331 }
2332
2333 // display the remaining text
2334 if (textPos != docText.text.size()) {
2335 m_stream << m_format.colors.result << QStringView(docText.text).sliced(textPos) << m_format.colors.reset;
2336 }
2337
2338 m_stream << '\n';
2339
2340 if (showCarret) {
2341 writeCarretLine(m_stream, m_format.colors, carretColumn);
2342 } else if (alignNL && docText.totalLine > 1 && !lastCall) {
2343 m_stream << '\n';
2344 }
2345 };
2346
2347 m_input.insertFormattingItems(textFormat);
2348 writeLabel(m_stream, m_format.colors, outputIsOk, " input: "_L1);
2349 writeText(m_input);
2350
2351 m_expected.insertFormattingItems(textFormat);
2352 writeLabel(m_stream, m_format.colors, outputIsOk, " output: "_L1);
2353 if (outputIsOk) {
2354 writeText(m_expected);
2355 } else {
2356 m_output.insertFormattingItems(textFormat);
2357
2358 /*
2359 * Compute carret position
2360 */
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()) {
2369 ++it1;
2370 continue;
2371 }
2372 if (!m_expected.blockSelection && it2->isBlockSelectionOrVirtual()) {
2373 ++it2;
2374 continue;
2375 }
2376
2377 if (differPos <= it1->pos || it1->pos != it2->pos || it1->kind != it2->kind
2378 || it1->virtualTextLen != (m_expected.blockSelection ? it2->virtualTextLen : 0)) {
2379 break;
2380 };
2381
2382 carretColumn += it1->virtualTextLen + replacements[it1->kind][1].size() - it1->isCharacter();
2383 if (alignNL && it1->kind == TextItem::NewLine) {
2384 ++carretLine;
2385 carretColumn = 0;
2386 ignoredLen = it1->pos + 1;
2387 }
2388
2389 ++it1;
2390 ++it2;
2391 }
2392 if (it1 != m_output.items.end() && it1->pos < differPos) {
2393 differPos = it1->pos;
2394 }
2395 if (it2 != m_expected.items.end() && it2->pos < differPos) {
2396 differPos = it2->pos;
2397 }
2398
2399 carretColumn += 12 + differPos - ignoredLen;
2400
2401 /*
2402 * Display output and expected output
2403 */
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);
2408 }
2409}
2410
2411void ScriptTester::writeSummary()
2412{
2413 auto &colors = m_format.colors;
2414
2415 if (m_failureCounter || m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation)) {
2416 m_stream << '\n';
2417 }
2418
2419 if (m_skipedCounter || m_breakOnErrorCounter) {
2420 m_stream << colors.labelInfo << "Test cases: Skipped: "_L1 << m_skipedCounter << " Aborted: "_L1 << m_breakOnErrorCounter << colors.reset << '\n';
2421 }
2422
2423 m_stream << "Success: "_L1 << colors.success << m_successCounter << colors.reset << " Failure: "_L1 << (m_failureCounter ? colors.error : colors.success)
2424 << m_failureCounter << colors.reset;
2425
2426 if (m_dualModeAbortedCounter) {
2427 m_stream << " DUAL_MODE aborted: "_L1 << colors.error << m_dualModeAbortedCounter << colors.reset;
2428 }
2429
2430 if (m_errorCounter) {
2431 m_stream << " Error: "_L1 << colors.error << m_errorCounter << colors.reset;
2432 }
2433
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;
2438 }
2439 }
2440}
2441
2442void ScriptTester::resetCounters()
2443{
2444 m_successCounter = 0;
2445 m_failureCounter = 0;
2446 m_xSuccessCounter = 0;
2447 m_xFailureCounter = 0;
2448 m_skipedCounter = 0;
2449 m_errorCounter = 0;
2450 m_breakOnErrorCounter = 0;
2451}
2452
2453void ScriptTester::type(const QString &str)
2454{
2455 m_doc->typeChars(m_view, str);
2456}
2457
2458void ScriptTester::enter()
2459{
2460 m_doc->newLine(m_view);
2461}
2462
2463void ScriptTester::paste(const QString &str)
2464{
2465 m_doc->paste(m_view, str);
2466}
2467
2468bool ScriptTester::testIndentFiles(const QString &name, const QString &dataDir, int nthStack, bool exitOnError)
2469{
2470 struct File {
2471 QString path;
2472 QString text;
2473 bool ok = true;
2474
2475 File(const QString &path)
2476 : path(path)
2477 {
2478 QFile file(path);
2479 if (file.open(QFile::ReadOnly | QFile::Text)) {
2480 text = QTextStream(&file).readAll();
2481 } else {
2482 ok = false;
2483 text = file.errorString();
2484 }
2485 }
2486 };
2487
2488 auto openError = [this](const QString &msg) {
2489 incrementError();
2490 m_engine->throwError(QJSValue::URIError, msg);
2491 return false;
2492 };
2493
2494 /*
2495 * Check directory
2496 */
2497
2498 const QString dirPath = QFileInfo(dataDir).isRelative() ? m_paths.indentBaseDir + u'/' + dataDir : dataDir;
2499 const QDir testDir(dirPath);
2500 if (!testDir.exists()) {
2501 return openError(testDir.path() + u" does not exist"_sv);
2502 }
2503
2504 /*
2505 * Read variable from .kateconfig
2506 */
2507
2508 QString variables;
2509 if (QFile kateConfig(dirPath + u"/.kateconfig"_sv); kateConfig.open(QFile::ReadOnly | QFile::Text)) {
2510 QTextStream stream(&kateConfig);
2511 QString line;
2512 while (stream.readLineInto(&line)) {
2513 if (line.startsWith(u"kate:"_s) && line.size() > 7) {
2514 variables += QStringView(line).sliced(5) + u';';
2515 }
2516 }
2517 }
2518 const auto variablesLen = variables.size();
2519 bool hasVariable = variablesLen;
2520
2521 /*
2522 * Indent each file in the folder
2523 */
2524
2525 initDocConfig();
2526
2527 const auto type = u"indent"_s;
2528 const auto program = u"view.align(document.documentRange())"_s;
2529 bool result = true;
2530 bool hasEntry = false;
2531
2532 const auto testList = testDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
2533 for (const auto &info : testList) {
2534 hasEntry = true;
2535 m_debugMsg.clear();
2536 m_hasDebugMessage = false;
2537
2538 const auto baseName = info.baseName();
2539 const QString name2 = name + u':' + baseName;
2540
2541 if (!startTestCase(name2, nthStack)) {
2542 continue;
2543 }
2544
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
2548 << ": "_L1;
2549 }
2550 };
2551
2552 const bool alwaysWriteTest = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteLocation);
2553 if (alwaysWriteTest) {
2554 writeLocation(nthStack);
2555 writeTestName();
2556 writeTypeAndProgram(type, program);
2557
2558 m_stream << m_format.colors.reset << ' ';
2559
2560 if (m_format.debugOptions.testAnyFlag(DebugOption::ForceFlush)) {
2561 m_stream.flush();
2562 }
2563 }
2564
2565 /*
2566 * Read input and expected output
2567 */
2568
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);
2574 }
2575 if (!expectedFile.ok) {
2576 return openError(expectedFile.path + u": " + expectedFile.text);
2577 }
2578
2579 /*
2580 * Set input
2581 */
2582
2583 // openUrl() blocks the program with the message, why ?
2584 // This plugin does not support propagateSizeHints()
2585 m_doc->setText(inputFile.text);
2586
2587 /*
2588 * Read local variables
2589 */
2590 auto appendVars = [&](int i) {
2591 const auto line = m_doc->line(i);
2592 if (line.contains("kate"_L1)) {
2593 variables += line + u';';
2594 }
2595 };
2596 const auto lines = m_doc->lines();
2597 for (int i = 0; i < qMin(9, lines); ++i) {
2598 appendVars(i);
2599 }
2600 if (lines > 10) {
2601 for (int i = qMax(10, lines - 10); i < lines; ++i) {
2602 appendVars(i);
2603 }
2604 }
2605
2606 /*
2607 * Set variables
2608 */
2609
2610 if (!variables.isEmpty()) {
2611 // setVariable() has no protection against multiple variable insertions
2612 m_doc->setVariable(u""_s, variables);
2613 syncIndenter();
2614 variables.resize(variablesLen);
2615 hasVariable = true;
2616 }
2617
2618 /*
2619 * Indent
2620 */
2621
2622 const auto selection = m_doc->documentRange();
2623 // TODO certain indenter like pascal requires that the lines be selection: this is probably an error
2624 m_view->setSelection(selection);
2625 m_doc->align(m_view, selection);
2626
2627 /*
2628 * Compare and show result
2629 */
2630
2631 const auto output = m_doc->text();
2632 const bool ok = output == expectedFile.text;
2633 const bool alwaysWriteInputOutput = m_format.testFormatOptions.testAnyFlag(TestFormatOption::AlwaysWriteInputOutput);
2634
2635 if (!alwaysWriteTest && (alwaysWriteInputOutput || !ok)) {
2636 writeLocation(nthStack);
2637 writeTestName();
2638 }
2639 if (!ok || alwaysWriteTest || alwaysWriteInputOutput) {
2640 if (ok) {
2641 m_stream << m_format.colors.success << "OK\n"_L1 << m_format.colors.reset;
2642 } else {
2643 m_stream << m_format.colors.error << "Output differs\n"_L1 << m_format.colors.reset;
2644 }
2645 }
2646 if (!alwaysWriteTest && (alwaysWriteInputOutput || !ok)) {
2647 writeTypeAndProgram(type, program);
2648 m_stream << ": \n"_L1;
2649 }
2650 if (!ok || alwaysWriteInputOutput) {
2651 m_stream << m_debugMsg;
2652 }
2653
2654 if (ok) {
2655 ++m_successCounter;
2656 } else {
2657 ++m_failureCounter;
2658
2659 const QString resultPath = dir + u"/actual"_sv;
2660
2661 /*
2662 * Write result file
2663 */
2664 {
2665 QFile outFile(resultPath);
2666 if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
2667 return openError(resultPath + u": "_sv + outFile.errorString());
2668 }
2669 QTextStream(&outFile) << output;
2670 }
2671
2672 /*
2673 * Elaborate diff output, if possible
2674 */
2675 if (!m_diffCmdLoaded) {
2676 m_diffCmd.path = QStandardPaths::findExecutable(m_diffCmd.path);
2677 m_diffCmdLoaded = true;
2678 }
2679 if (!m_diffCmd.path.isEmpty()) {
2680 m_stream.flush();
2681 QProcess proc;
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);
2687 // disable timeout in case of diff with pager (as `delta` or `wdiff`)
2688 if (!proc.waitForFinished(-1) || !proc.exitCode()) {
2689 incrementError();
2690 m_engine->throwError(u"diff command error"_s);
2691 return false;
2692 }
2693 }
2694 /*
2695 * else: trivial output of mismatching characters, e.g. for windows testing without diff
2696 */
2697 else {
2698 qDebug() << "Trivial differences output as the 'diff' executable is not in the PATH";
2699 m_stream << "--- "_L1 << expectedFile.path << "\n+++ " << resultPath << '\n';
2700 const auto expectedLines = QStringView(expectedFile.text).split(u'\n');
2701 const auto outputLines = QStringView(output).split(u'\n');
2702 const auto minLine = qMin(expectedLines.size(), outputLines.size());
2703 qsizetype i = 0;
2704 for (; i < minLine; ++i) {
2705 if (expectedLines[i] == outputLines[i]) {
2706 m_stream << " "_L1 << expectedLines[i] << '\n';
2707 } else {
2708 m_stream << "- "_L1 << expectedLines[i] << "\n+ "_L1 << outputLines[i] << '\n';
2709 }
2710 }
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';
2717 }
2718 }
2719 }
2720
2721 if (exitOnError || hasTooManyErrors()) {
2722 return false;
2723 }
2724
2725 result = false;
2726 }
2727 }
2728
2729 if (!hasEntry) {
2730 incrementError();
2731 m_engine->throwError(testDir.path() + u" is empty"_sv);
2732 return false;
2733 }
2734
2735 m_editorConfig.updated = !hasVariable;
2736
2737 return result;
2738}
2739
2740} // namespace KTextEditor
2741
2742#include "moc_scripttester_p.cpp"
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
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
QJSValue catchError()
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)
void clear()
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
iterator end()
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
QString readAll()
bool readLineInto(QString *line, qint64 maxlen)
void reset()
void setFieldWidth(int width)
void setPadChar(QChar ch)
iterator begin()
void resize(qsizetype size)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:11:27 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.