| 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 | |