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 | |
24 | namespace KTextEditor |
25 | { |
26 | |
27 | using namespace Qt::Literals::StringLiterals; |
28 | |
29 | namespace |
30 | { |
31 | |
32 | /** |
33 | * UDL for QStringView |
34 | */ |
35 | constexpr 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 | */ |
49 | static 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(fileName: path)) { |
54 | return path; |
55 | } |
56 | } |
57 | |
58 | *error = u"file '%1' not found in %2"_sv .arg(args: name, args: dirs.join(sep: u", "_sv )); |
59 | engine->throwError(errorType: QJSValue::URIError, message: *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 | */ |
72 | static 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(fileName: 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(args: fileName); |
91 | } else { |
92 | *error = u"file '%1' not found in %2 and working directory"_sv .arg(args: fileName, args: dirs.join(sep: u", "_sv )); |
93 | } |
94 | |
95 | engine->throwError(errorType: QJSValue::URIError, message: *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 | */ |
107 | static bool readFile(QJSEngine *engine, const QString &sourceUrl, QString *content, QString *error) |
108 | { |
109 | QFile file(sourceUrl); |
110 | if (file.open(flags: 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(args: sourceUrl, args: file.errorString()); |
117 | engine->throwError(errorType: QJSValue::URIError, message: *error); |
118 | return false; |
119 | } |
120 | |
121 | /** |
122 | * Write a line with "^~~" at the position \c column. |
123 | */ |
124 | static 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 | */ |
139 | static 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 | */ |
151 | template<class SetFn> |
152 | static 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 | */ |
163 | template<class SetFn> |
164 | static 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 | */ |
175 | template<class SetFn> |
176 | static 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 | */ |
187 | static qsizetype computeOffsetDifference(QStringView a, QStringView b) |
188 | { |
189 | qsizetype n = qMin(a: a.size(), b: 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(a: 0, b: i - 1); |
195 | } |
196 | return i; |
197 | } |
198 | } |
199 | return i; |
200 | } |
201 | |
202 | struct 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 | */ |
216 | static VirtualText findVirtualText(QStringView str, qsizetype pos, QChar c) |
217 | { |
218 | VirtualText res{.pos1: str.indexOf(c, from: pos), .pos2: -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 | */ |
231 | static qsizetype filePrefixLen(QStringView str) |
232 | { |
233 | // skip file prefix |
234 | if (str.startsWith(s: "file://"_L1 )) { |
235 | return 7; |
236 | } else if (str.startsWith(s: "file:"_L1 )) { |
237 | return 5; |
238 | } else if (str.startsWith(s: "qrc:"_L1 )) { |
239 | return 3; |
240 | } |
241 | return 0; |
242 | } |
243 | |
244 | /** |
245 | * @return \p str without its file prefix (\see filePrefixLen). |
246 | */ |
247 | static QStringView skipFilePrefix(QStringView str) |
248 | { |
249 | return str.sliced(pos: filePrefixLen(str)); |
250 | } |
251 | |
252 | /** |
253 | * @return stack property of js Error. |
254 | */ |
255 | static inline QJSValue getStack(const QJSValue &exception) |
256 | { |
257 | return exception.property(name: u"stack"_s ); |
258 | } |
259 | |
260 | /** |
261 | * @return stack of \p engine. |
262 | */ |
263 | static inline QJSValue generateStack(QJSEngine *engine) |
264 | { |
265 | engine->throwError(message: QString()); |
266 | return getStack(exception: engine->catchError()); |
267 | } |
268 | |
269 | struct 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 | */ |
281 | static StackLine parseStackLine(QStringView stack) |
282 | { |
283 | // format: funcName? '@file:' '//'? fileName ':' lineNumber '\n' |
284 | |
285 | StackLine ret; |
286 | |
287 | // func |
288 | qsizetype pos = stack.indexOf(c: '@'_L1); |
289 | if (pos >= 0) { |
290 | ret.funcName = stack.first(n: pos); |
291 | stack = stack.sliced(pos: pos + 1); |
292 | } |
293 | |
294 | // remove file prefix |
295 | pos = filePrefixLen(str: stack); |
296 | ret.filePrefixOrMessage = stack.first(n: pos); |
297 | |
298 | auto endLine = stack.indexOf(c: '\n'_L1, from: pos); |
299 | auto line = stack.sliced(pos, n: ((endLine < 0) ? stack.size() : endLine) - pos); |
300 | // fileName and lineNumber |
301 | auto i = line.lastIndexOf(c: ':'_L1); |
302 | if (i > 0) { |
303 | ret.fileName = line.sliced(pos: 0, n: i); |
304 | ret.lineNumber = line.sliced(pos: i + 1); |
305 | } else { |
306 | ret.filePrefixOrMessage = line; |
307 | } |
308 | |
309 | if (endLine >= 0) { |
310 | ret.remaining = stack.sliced(pos: 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 | */ |
323 | static 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(c: '\n'_L1); |
328 | if (pos < 0) { |
329 | buffer += colors.error % prefix % stack % colors.reset % u'\n'; |
330 | return; |
331 | } |
332 | stack = stack.sliced(pos: 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 | |
351 | static inline bool cursorSameAsSecondary(const ScriptTester::Placeholders &placeholders) |
352 | { |
353 | return placeholders.cursor == placeholders.secondaryCursor; |
354 | } |
355 | |
356 | static inline bool selectionStartSameAsSecondary(const ScriptTester::Placeholders &placeholders) |
357 | { |
358 | return placeholders.selectionStart == placeholders.secondarySelectionStart; |
359 | } |
360 | |
361 | static inline bool selectionEndSameAsSecondary(const ScriptTester::Placeholders &placeholders) |
362 | { |
363 | return placeholders.selectionEnd == placeholders.secondarySelectionEnd; |
364 | } |
365 | |
366 | static inline bool selectionSameAsSecondary(const ScriptTester::Placeholders &placeholders) |
367 | { |
368 | return selectionStartSameAsSecondary(placeholders) || selectionEndSameAsSecondary(placeholders); |
369 | } |
370 | |
371 | } // anonymous namespace |
372 | |
373 | ScriptTester::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 | */ |
391 | struct 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 | |
545 | ScriptTester::DocumentText::DocumentText() = default; |
546 | ScriptTester::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 | */ |
555 | std::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, from: pos))) { |
561 | items.push_back(x: {.pos: pos, .kind: 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 | */ |
579 | std::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(c: start, from: pos))) { |
585 | qsizetype pos2 = str.indexOf(c: end, from: 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(x: {.pos: pos, .kind: TextItem::Kind(kind + offset1)}); |
602 | items.push_back(x: {.pos: pos2, .kind: 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 | */ |
613 | void 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(new_size: 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] = {.pos: text.size(), .kind: TextItem::NewLine, .virtualTextLen: 0}; |
645 | } else { |
646 | items.resize(new_size: 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 = {.pos: textPos + column, .kind: kind}; |
692 | } else /*if (first->virtualTextLen || first->kind == TextItem::NewLine)*/ { |
693 | vlen = column - (itemIt->pos - textPos); |
694 | *outIt = {.pos: itemIt->pos, .kind: kind, .virtualTextLen: 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(a: itemIt->virtualTextLen, b: 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(a&: leftColumn, b&: 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(a: itemIt->virtualTextLen, b: 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(a: itemIt->virtualTextLen, b: 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 | */ |
755 | void 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(str: text, kind: TextItem::Backslash, c: u'\\'); |
770 | addItems(str: text, kind: TextItem::DoubleQuote, c: u'"'); |
771 | [[fallthrough]]; |
772 | case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral: |
773 | case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder: |
774 | case DocumentTextFormat::ReplaceTabWithPlaceholder: |
775 | addItems(str: text, kind: TextItem::Tab, c: 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 | */ |
793 | void 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(first: items.begin(), last: items.end(), comp: 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 | */ |
819 | QString 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(str: input, kind: TextItem::NewLine, c: 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(str: input, kind: TextItem::SecondaryCursor, c: 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(str: input, kind: TextItem::Cursor, c: 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(str: input, kind: TextItem::SecondarySelectionStart, start: placeholders.secondarySelectionStart, end: 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(str: input, kind: TextItem::SelectionStart, start: placeholders.selectionStart, end: 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{.pos1: -1, .pos2: -1}; |
905 | |
906 | if (placeholders.hasVirtualText()) { |
907 | virtualText = findVirtualText(str: input, pos: 0, c: 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(v: input.sliced(pos, n: item.pos - pos - virtualText.size())); |
962 | |
963 | ignoredChars += virtualText.size(); |
964 | virtualTextLen += virtualText.size(); |
965 | virtualText = findVirtualText(str: input, pos: virtualText.pos2, c: 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(x: {.pos: cursorFromCurrentItem(), .range: 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(t: {.pos: Cursor::invalid(), .range: {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(v: input.sliced(pos, n: 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(a: 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(pos: 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(a: 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(t: c); |
1128 | } |
1129 | } |
1130 | if (n != secondaryCursorsWithSelection.size()) { |
1131 | std::sort(first: secondaryCursorsWithSelection.begin(), last: 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(first: secondaryCursors.begin(), last: 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(position: afterSelection, x: {.pos: selectionEndItem->pos, .kind: TextItem::Cursor, .virtualTextLen: 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(x: {.pos: input.size(), .kind: TextItem::Cursor, .virtualTextLen: 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 | |
1168 | ScriptTester::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 | |
1198 | QString 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(engine: m_engine, name, dirs: m_paths.scripts, error: &contentOrError); |
1205 | if (!fullName.isEmpty()) { |
1206 | readFile(engine: m_engine, sourceUrl: fullName, content: &contentOrError, error: &contentOrError); |
1207 | } |
1208 | return contentOrError; |
1209 | } |
1210 | |
1211 | void ScriptTester::require(const QString &name) |
1212 | { |
1213 | // check include guard |
1214 | auto it = m_libraryFiles.find(key: name); |
1215 | if (it != m_libraryFiles.end()) { |
1216 | // re-throw previous exception |
1217 | if (!it->isEmpty()) { |
1218 | m_engine->throwError(errorType: QJSValue::URIError, message: *it); |
1219 | } |
1220 | return; |
1221 | } |
1222 | |
1223 | it = m_libraryFiles.insert(key: name, value: QString()); |
1224 | |
1225 | QString fullName = getPath(engine: m_engine, name, dirs: m_paths.libraries, error: &*it); |
1226 | if (fullName.isEmpty()) { |
1227 | return; |
1228 | } |
1229 | |
1230 | QString program; |
1231 | if (!readFile(engine: m_engine, sourceUrl: fullName, content: &program, error: &*it)) { |
1232 | return; |
1233 | } |
1234 | |
1235 | // eval in current script engine |
1236 | const QJSValue val = m_engine->evaluate(program, fileName: fullName); |
1237 | if (!val.isError()) { |
1238 | return; |
1239 | } |
1240 | |
1241 | // propagate exception |
1242 | *it = val.toString(); |
1243 | m_engine->throwError(error: val); |
1244 | } |
1245 | |
1246 | void ScriptTester::debug(const QString &message) |
1247 | { |
1248 | const auto requireStack = m_format.debugOptions.testAnyFlags(flags: DebugOption::WriteStackTrace | DebugOption::WriteFunction); |
1249 | const auto err = m_format.debugOptions ? generateStack(engine: 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(flag: DebugOption::WriteLocation)) { |
1263 | auto pushLocation = [this](QStringView fileName, QStringView lineNumber) { |
1264 | m_debugMsg += m_format.colors.fileName % skipFilePrefix(str: 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(name: u"fileName"_s ); |
1268 | // qrc file has no fileName |
1269 | if (fileName.isUndefined()) { |
1270 | auto stack2 = requireStack ? stack : m_format.debugOptions ? err.toString() : generateStack(engine: m_engine).toString(); |
1271 | auto stackLine = parseStackLine(stack); |
1272 | pushLocation(stackLine.fileName, stackLine.lineNumber); |
1273 | } else { |
1274 | pushLocation(fileName.toString(), err.property(name: u"lineNumber"_s ).toString()); |
1275 | } |
1276 | } |
1277 | |
1278 | // add {funcName}: |
1279 | if (m_format.debugOptions.testAnyFlag(flag: DebugOption::WriteFunction)) { |
1280 | const QStringView stackView = stack; |
1281 | const qsizetype pos = stackView.indexOf(c: '@'_L1); |
1282 | if (pos > 0) { |
1283 | m_debugMsg += m_format.colors.program % stackView.first(n: 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(flag: DebugOption::WriteStackTrace)) { |
1293 | pushException(buffer&: m_debugMsg, colors&: m_format.colors, stack, prefix: u"| "_sv ); |
1294 | } |
1295 | |
1296 | // flush |
1297 | if (m_format.debugOptions.testAnyFlag(flag: DebugOption::ForceFlush)) { |
1298 | if (!m_hasDebugMessage && m_format.testFormatOptions.testAnyFlag(flag: 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 | |
1309 | void ScriptTester::print(const QString &message) |
1310 | { |
1311 | if (m_format.debugOptions.testAnyFlags(flags: 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(engine: m_engine).toString(); |
1321 | QStringView err = errStr; |
1322 | auto nl = err.indexOf(c: u'\n'); |
1323 | if (nl != -1) { |
1324 | auto stackLine = parseStackLine(stack: err.sliced(pos: nl + 1)); |
1325 | |
1326 | // add {fileName}:{lineNumber}: |
1327 | if (m_format.debugOptions.testAnyFlag(flag: DebugOption::WriteLocation)) { |
1328 | m_stream << m_format.colors.fileName << skipFilePrefix(str: 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(flag: 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 | |
1346 | QJSValue ScriptTester::loadModule(const QString &fileName) |
1347 | { |
1348 | QString error; |
1349 | const auto path = getModulePath(engine: m_engine, fileName, dirs: m_paths.modules, error: &error); |
1350 | if (path.isEmpty()) { |
1351 | return QJSValue(); |
1352 | } |
1353 | |
1354 | auto mod = m_engine->importModule(fileName: path); |
1355 | if (mod.isError()) { |
1356 | m_engine->throwError(error: mod); |
1357 | } |
1358 | return mod; |
1359 | } |
1360 | |
1361 | void ScriptTester::loadScript(const QString &fileName) |
1362 | { |
1363 | QString contentOrError; |
1364 | const auto path = getModulePath(engine: m_engine, fileName, dirs: m_paths.scripts, error: &contentOrError); |
1365 | if (path.isEmpty()) { |
1366 | return; |
1367 | } |
1368 | |
1369 | if (!readFile(engine: m_engine, sourceUrl: path, content: &contentOrError, error: &contentOrError)) { |
1370 | return; |
1371 | } |
1372 | |
1373 | // eval in current script engine |
1374 | const QJSValue val = m_engine->evaluate(program: contentOrError, fileName); |
1375 | if (!val.isError()) { |
1376 | return; |
1377 | } |
1378 | |
1379 | // propagate exception |
1380 | m_engine->throwError(error: val); |
1381 | } |
1382 | |
1383 | bool 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(subjectView: 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(flag: DebugOption::ForceFlush)) { |
1408 | m_stream.flush(); |
1409 | } |
1410 | |
1411 | return false; |
1412 | } |
1413 | |
1414 | void 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(obj: config, name, setFn: [&](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(obj: config, name: fallbackName, setFn: [&](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 | |
1483 | void ScriptTester::resetConfig() |
1484 | { |
1485 | m_fallbackPlaceholders = m_format.fallbackPlaceholders; |
1486 | m_placeholders = m_defaultPlaceholders; |
1487 | m_editorConfig = makeEditorConfig(); |
1488 | m_configStack.clear(); |
1489 | } |
1490 | |
1491 | void ScriptTester::pushConfig() |
1492 | { |
1493 | m_configStack.emplace_back(args: Config{.fallbackPlaceholders: m_fallbackPlaceholders, .placeholders: m_placeholders, .editorConfig: m_editorConfig}); |
1494 | m_editorConfig.inherited = true; |
1495 | } |
1496 | |
1497 | void 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 | |
1511 | QJSValue ScriptTester::evaluate(const QString &program) |
1512 | { |
1513 | QStringList stack; |
1514 | auto err = m_engine->evaluate(program, fileName: u"(program)"_s , lineNumber: 1, exceptionStackTrace: &stack); |
1515 | if (!stack.isEmpty()) { |
1516 | m_engine->throwError(error: err); |
1517 | } |
1518 | return err; |
1519 | } |
1520 | |
1521 | void ScriptTester::setInput(const QString &input, bool blockSelection) |
1522 | { |
1523 | auto err = m_input.setText(input, placeholders: m_placeholders); |
1524 | if (err.isEmpty() && checkMultiCursorCompatibility(doc: m_input, blockSelection, err: &err)) { |
1525 | m_input.blockSelection = blockSelection; |
1526 | initInputDoc(); |
1527 | } else { |
1528 | m_engine->throwError(message: err); |
1529 | ++m_errorCounter; |
1530 | } |
1531 | } |
1532 | |
1533 | void ScriptTester::moveExpectedOutputToInput(bool blockSelection) |
1534 | { |
1535 | // prefer swap to std::move to avoid freeing vector / list memory |
1536 | std::swap(a&: m_input, b&: m_expected); |
1537 | reuseInput(blockSelection); |
1538 | } |
1539 | |
1540 | void ScriptTester::reuseInput(bool blockSelection) |
1541 | { |
1542 | QString err; |
1543 | if (checkMultiCursorCompatibility(doc: m_input, blockSelection, err: &err)) { |
1544 | m_input.blockSelection = blockSelection; |
1545 | initInputDoc(); |
1546 | } else { |
1547 | m_engine->throwError(message: err); |
1548 | ++m_errorCounter; |
1549 | } |
1550 | } |
1551 | |
1552 | bool ScriptTester::reuseInputWithBlockSelection() |
1553 | { |
1554 | QString err; |
1555 | if (!checkMultiCursorCompatibility(doc: m_input, blockSelection: true, err: &err)) { |
1556 | return false; |
1557 | } |
1558 | m_input.blockSelection = true; |
1559 | initInputDoc(); |
1560 | return true; |
1561 | } |
1562 | |
1563 | bool 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 | |
1579 | void ScriptTester::initDocConfig() |
1580 | { |
1581 | if (m_editorConfig.updated) { |
1582 | return; |
1583 | } |
1584 | |
1585 | m_editorConfig.updated = true; |
1586 | |
1587 | m_view->config()->setValue(key: KateViewConfig::AutoBrackets, value: 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 | |
1602 | void 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 | |
1610 | void 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(cursorsWithSelection: m_input.secondaryCursorsWithSelection); |
1623 | } |
1624 | } |
1625 | |
1626 | void ScriptTester::setExpectedOutput(const QString &expected, bool blockSelection) |
1627 | { |
1628 | auto err = m_expected.setText(input: expected, placeholders: m_placeholders); |
1629 | if (err.isEmpty() && checkMultiCursorCompatibility(doc: m_expected, blockSelection, err: &err)) { |
1630 | m_expected.blockSelection = blockSelection; |
1631 | } else { |
1632 | m_engine->throwError(message: err); |
1633 | ++m_errorCounter; |
1634 | } |
1635 | } |
1636 | |
1637 | void ScriptTester::reuseExpectedOutput(bool blockSelection) |
1638 | { |
1639 | QString err; |
1640 | if (checkMultiCursorCompatibility(doc: m_expected, blockSelection, err: &err)) { |
1641 | m_expected.blockSelection = blockSelection; |
1642 | } else { |
1643 | m_engine->throwError(message: err); |
1644 | ++m_errorCounter; |
1645 | } |
1646 | } |
1647 | |
1648 | void ScriptTester::copyInputToExpectedOutput(bool blockSelection) |
1649 | { |
1650 | m_expected = m_input; |
1651 | reuseExpectedOutput(blockSelection); |
1652 | } |
1653 | |
1654 | bool 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(new_size: 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(line: expected.line()); |
1690 | int column = qMin(a: lineLen, b: 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(first1: m_output.secondaryCursors.begin(), |
1705 | last1: m_output.secondaryCursors.end(), |
1706 | first2: m_expected.secondaryCursorsWithSelection.constBegin(), |
1707 | last2: m_expected.secondaryCursorsWithSelection.constEnd(), |
1708 | binary_pred: SecondaryEq)) { |
1709 | return true; |
1710 | } |
1711 | } else if (m_output.cursor == m_expected.cursor && m_output.selection == m_expected.selection |
1712 | && std::equal(first1: m_output.secondaryCursors.begin(), |
1713 | last1: m_output.secondaryCursors.end(), |
1714 | first2: m_expected.secondaryCursorsWithSelection.constBegin(), |
1715 | last2: m_expected.secondaryCursorsWithSelection.constEnd(), |
1716 | binary_pred: [](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 | }; |
1731 | QVarLengthArray<CursorItem, 12> cursorItems; |
1732 | cursorItems.resize(sz: m_output.secondaryCursors.size() * 3 + 3); |
1733 | |
1734 | auto it = cursorItems.begin(); |
1735 | if (m_output.cursor.isValid()) { |
1736 | *it++ = {.cursor: m_output.cursor, .kind: 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++ = {.cursor: m_output.selection.start(), .kind: start}; |
1743 | *it++ = {.cursor: m_output.selection.end(), .kind: end}; |
1744 | } |
1745 | for (const auto &c : m_output.secondaryCursors) { |
1746 | *it++ = {.cursor: c.pos, .kind: 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++ = {.cursor: c.range.start(), .kind: start}; |
1752 | *it++ = {.cursor: c.range.end(), .kind: end}; |
1753 | } |
1754 | } |
1755 | |
1756 | const auto end = it; |
1757 | std::sort(first: cursorItems.begin(), last: end, comp: [](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(c: u'\n', from: 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(x: {.pos: pos + it->cursor.column() - virtualTextLen, .kind: it->kind, .virtualTextLen: virtualTextLen}); |
1786 | } |
1787 | if (nextPos == -1) { |
1788 | break; |
1789 | } |
1790 | m_output.items.push_back(x: {.pos: nextPos, .kind: TextItem::NewLine, .virtualTextLen: 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 | |
1801 | bool 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 | |
1817 | void ScriptTester::incrementError() |
1818 | { |
1819 | ++m_errorCounter; |
1820 | } |
1821 | |
1822 | void ScriptTester::incrementBreakOnError() |
1823 | { |
1824 | ++m_breakOnErrorCounter; |
1825 | } |
1826 | |
1827 | int ScriptTester::countError() const |
1828 | { |
1829 | return m_errorCounter + m_failureCounter + m_xFailureCounter; |
1830 | } |
1831 | |
1832 | bool ScriptTester::hasTooManyErrors() const |
1833 | { |
1834 | return m_executionConfig.maxError > 0 && countError() >= m_executionConfig.maxError; |
1835 | } |
1836 | |
1837 | int ScriptTester::startTest() |
1838 | { |
1839 | m_debugMsg.clear(); |
1840 | m_hasDebugMessage = false; |
1841 | int flags = 0; |
1842 | flags |= m_format.testFormatOptions.testAnyFlag(flag: TestFormatOption::AlwaysWriteInputOutput) ? 1 : 0; |
1843 | flags |= m_format.testFormatOptions.testAnyFlag(flag: TestFormatOption::AlwaysWriteLocation) ? 2 : 0; |
1844 | return flags; |
1845 | } |
1846 | |
1847 | void 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 | |
1865 | void 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(flag: DebugOption::ForceFlush)) { |
1882 | m_stream.flush(); |
1883 | } |
1884 | } |
1885 | |
1886 | void 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 | |
1895 | void ScriptTester::writeTestName(const QString &name) |
1896 | { |
1897 | if (!m_format.testFormatOptions.testAnyFlag(flag: TestFormatOption::HiddenTestName) && !name.isEmpty()) { |
1898 | m_stream << m_format.colors.testName << name << m_format.colors.reset << ": "_L1 ; |
1899 | } |
1900 | } |
1901 | |
1902 | void 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 | |
1908 | void 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(flag: TestFormatOption::AlwaysWriteLocation); |
1927 | const bool alwaysWriteInputOutput = m_format.testFormatOptions.testAnyFlag(flag: 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(sameInputOutput: 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(stream&: m_stream, colors: m_format.colors, colored: false, text: " 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(stream&: m_stream, colors: m_format.colors, colored: options & sameResultOrError, text: label); |
2027 | |
2028 | m_stream << m_format.colors.result << result << m_format.colors.reset << '\n'; |
2029 | if (!(options & sameResultOrError)) { |
2030 | auto differPos = computeOffsetDifference(a: result, b: expectedResult); |
2031 | m_stream << " expected: "_L1 ; |
2032 | m_stream << m_format.colors.result << expectedResult << m_format.colors.reset << '\n'; |
2033 | writeCarretLine(stream&: m_stream, colors: m_format.colors, column: 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, prefix: u" | "_sv ); |
2044 | } |
2045 | |
2046 | m_stream << '\n'; |
2047 | } |
2048 | |
2049 | void 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(buffer&: m_stringBuffer, colors&: m_format.colors, stack: stack.toString(), prefix); |
2057 | m_stream << m_stringBuffer; |
2058 | } |
2059 | } |
2060 | |
2061 | void ScriptTester::writeLocation(int nthStack) |
2062 | { |
2063 | const auto errStr = generateStack(engine: 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(c: '\n'_L1, from: startIndex); |
2070 | if (pos <= -1) { |
2071 | break; |
2072 | } |
2073 | startIndex = pos + 1; |
2074 | } |
2075 | |
2076 | auto stackLine = parseStackLine(stack: err.sliced(pos: 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 | */ |
2084 | struct 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(a: doc->config()->tabWidth(), b: 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 | |
2217 | void 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(colors: m_format.colors); |
2232 | alignNL = false; |
2233 | break; |
2234 | case DocumentTextFormat::ReplaceNewLineAndTabWithLiteral: |
2235 | replacements.initReplaceNewLineAndTabWithLiteral(colors: m_format.colors); |
2236 | alignNL = false; |
2237 | break; |
2238 | case DocumentTextFormat::ReplaceNewLineAndTabWithPlaceholder: |
2239 | replacements.initNewLine(format: m_format); |
2240 | [[fallthrough]]; |
2241 | case DocumentTextFormat::ReplaceTabWithPlaceholder: |
2242 | replacements.initTab(format: m_format, doc: 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, reverseSelection: 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(pos: textPos, n: 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(pos: 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(stream&: m_stream, colors: m_format.colors, column: 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(pos: textPos) << m_format.colors.reset; |
2336 | } |
2337 | |
2338 | m_stream << '\n'; |
2339 | |
2340 | if (showCarret) { |
2341 | writeCarretLine(stream&: m_stream, colors: m_format.colors, column: carretColumn); |
2342 | } else if (alignNL && docText.totalLine > 1 && !lastCall) { |
2343 | m_stream << '\n'; |
2344 | } |
2345 | }; |
2346 | |
2347 | m_input.insertFormattingItems(format: textFormat); |
2348 | writeLabel(stream&: m_stream, colors: m_format.colors, colored: outputIsOk, text: " input: "_L1 ); |
2349 | writeText(m_input); |
2350 | |
2351 | m_expected.insertFormattingItems(format: textFormat); |
2352 | writeLabel(stream&: m_stream, colors: m_format.colors, colored: outputIsOk, text: " output: "_L1 ); |
2353 | if (outputIsOk) { |
2354 | writeText(m_expected); |
2355 | } else { |
2356 | m_output.insertFormattingItems(format: textFormat); |
2357 | |
2358 | /* |
2359 | * Compute carret position |
2360 | */ |
2361 | qsizetype carretLine = 0; |
2362 | qsizetype carretColumn = 0; |
2363 | qsizetype ignoredLen = 0; |
2364 | auto differPos = computeOffsetDifference(a: m_output.text, b: 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 | |
2411 | void ScriptTester::writeSummary() |
2412 | { |
2413 | auto &colors = m_format.colors; |
2414 | |
2415 | if (m_failureCounter || m_format.testFormatOptions.testAnyFlag(flag: 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 | |
2442 | void 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 | |
2453 | void ScriptTester::type(const QString &str) |
2454 | { |
2455 | m_doc->typeChars(view: m_view, chars: str); |
2456 | } |
2457 | |
2458 | void ScriptTester::enter() |
2459 | { |
2460 | m_doc->newLine(view: m_view); |
2461 | } |
2462 | |
2463 | void ScriptTester::paste(const QString &str) |
2464 | { |
2465 | m_doc->paste(view: m_view, text: str); |
2466 | } |
2467 | |
2468 | bool 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(flags: 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(errorType: QJSValue::URIError, message: 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(flags: QFile::ReadOnly | QFile::Text)) { |
2510 | QTextStream stream(&kateConfig); |
2511 | QString line; |
2512 | while (stream.readLineInto(line: &line)) { |
2513 | if (line.startsWith(s: u"kate:"_s ) && line.size() > 7) { |
2514 | variables += QStringView(line).sliced(pos: 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(filters: QDir::Dirs | QDir::NoDotAndDotDot, sort: 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(name: name2, nthStack)) { |
2542 | continue; |
2543 | } |
2544 | |
2545 | auto writeTestName = [&] { |
2546 | if (!m_format.testFormatOptions.testAnyFlag(flag: 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(flag: 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(flag: 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(line: i); |
2592 | if (line.contains(s: "kate"_L1 )) { |
2593 | variables += line + u';'; |
2594 | } |
2595 | }; |
2596 | const auto lines = m_doc->lines(); |
2597 | for (int i = 0; i < qMin(a: 9, b: lines); ++i) { |
2598 | appendVars(i); |
2599 | } |
2600 | if (lines > 10) { |
2601 | for (int i = qMax(a: 10, b: 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(name: u""_s , value: variables); |
2613 | syncIndenter(); |
2614 | variables.resize(size: 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(view: m_view, range: 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(flag: 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(flags: 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(executableName: m_diffCmd.path); |
2677 | m_diffCmdLoaded = true; |
2678 | } |
2679 | if (!m_diffCmd.path.isEmpty()) { |
2680 | m_stream.flush(); |
2681 | QProcess proc; |
2682 | proc.setProcessChannelMode(QProcess::ForwardedChannels); |
2683 | m_diffCmd.args.push_back(t: expectedFile.path); |
2684 | m_diffCmd.args.push_back(t: resultPath); |
2685 | proc.start(program: m_diffCmd.path, arguments: m_diffCmd.args); |
2686 | m_diffCmd.args.resize(size: m_diffCmd.args.size() - 2); |
2687 | // disable timeout in case of diff with pager (as `delta` or `wdiff`) |
2688 | if (!proc.waitForFinished(msecs: -1) || !proc.exitCode()) { |
2689 | incrementError(); |
2690 | m_engine->throwError(message: 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(sep: u'\n'); |
2701 | const auto outputLines = QStringView(output).split(sep: u'\n'); |
2702 | const auto minLine = qMin(a: expectedLines.size(), b: 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(message: 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" |
2743 | |