KTextEditor

ktexteditorscripttester.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Jonathan Poelen <jonathan.poelen@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "kateconfig.h"
8#include "katedocument.h"
9#include "katescriptdocument.h"
10#include "katescriptview.h"
11#include "kateview.h"
12#include "ktexteditor_version.h"
13#include "scripttester_p.h"
14
15#include <chrono>
16
17#include <QApplication>
18#include <QCommandLineOption>
19#include <QCommandLineParser>
20#include <QDirIterator>
21#include <QFile>
22#include <QFileInfo>
23#include <QJSEngine>
24#include <QStandardPaths>
25#include <QVarLengthArray>
26#include <QtEnvironmentVariables>
27#include <QtLogging>
28
29using namespace Qt::Literals::StringLiterals;
30
31namespace
32{
33
34constexpr QStringView operator""_sv(const char16_t *str, size_t size) noexcept
35{
36 return QStringView(str, size);
37}
38
39using ScriptTester = KTextEditor::ScriptTester;
40using TextFormat = ScriptTester::DocumentTextFormat;
41using TestFormatOption = ScriptTester::TestFormatOption;
42using PatternType = ScriptTester::PatternType;
43using DebugOption = ScriptTester::DebugOption;
44
45constexpr inline ScriptTester::Format::TextReplacement defaultTextReplacement{
46 .newLine = u'↵',
47 .tab1 = u'—',
48 .tab2 = u'⇥',
49};
50constexpr inline ScriptTester::Placeholders defaultPlaceholder{
51 .cursor = u'|',
52 .selectionStart = u'[',
53 .selectionEnd = u']',
54 .secondaryCursor = u'\0',
55 .secondarySelectionStart = u'\0',
56 .secondarySelectionEnd = u'\0',
57 .virtualText = u'\0',
58};
59constexpr inline ScriptTester::Placeholders defaultFallbackPlaceholders{
60 .cursor = u'|',
61 .selectionStart = u'[',
62 .selectionEnd = u']',
63 .secondaryCursor = u'┆',
64 .secondarySelectionStart = u'❲',
65 .secondarySelectionEnd = u'❳',
66 .virtualText = u'·',
67};
68
69enum class DualMode {
70 Dual,
71 NoBlockSelection,
72 BlockSelection,
73 DualIsAlwaysDual,
74 AlwaysDualIsDual,
75};
76
77struct ScriptTesterQuery {
78 ScriptTester::Format format{.debugOptions = DebugOption::WriteLocation | DebugOption::WriteFunction,
79 .testFormatOptions = TestFormatOption::None,
80 .documentTextFormat = TextFormat::ReplaceNewLineAndTabWithLiteral,
81 .documentTextFormatWithBlockSelection = TextFormat::ReplaceNewLineAndTabWithPlaceholder,
82 .textReplacement = defaultTextReplacement,
83 .fallbackPlaceholders = defaultFallbackPlaceholders,
84 .colors = {
85 .reset = u"\033[m"_s,
86 .success = u"\033[32m"_s,
87 .error = u"\033[31m"_s,
88 .carret = u"\033[31m"_s,
89 .debugMarker = u"\033[31;1m"_s,
90 .debugMsg = u"\033[31m"_s,
91 .testName = u"\x1b[36m"_s,
92 .program = u"\033[32m"_s,
93 .fileName = u"\x1b[34m"_s,
94 .lineNumber = u"\x1b[35m"_s,
95 .blockSelectionInfo = u"\x1b[37m"_s,
96 .labelInfo = u"\x1b[37m"_s,
97 .cursor = u"\x1b[40;1;33m"_s,
98 .selection = u"\x1b[40;1;33m"_s,
99 .secondaryCursor = u"\x1b[40;33m"_s,
100 .secondarySelection = u"\x1b[40;33m"_s,
101 .blockSelection = u"\x1b[40;37m"_s,
102 .inSelection = u"\x1b[4m"_s,
103 .virtualText = u"\x1b[40;37m"_s,
104 .result = u"\x1b[40m"_s,
105 .resultReplacement = u"\x1b[40;36m"_s,
106 }};
107
108 ScriptTester::Paths paths{
109 .scripts = {},
110 .libraries = {u":/ktexteditor/script/libraries"_s},
111 .files = {},
112 .modules = {},
113 .indentBaseDir = {},
114 };
115
116 ScriptTester::TestExecutionConfig executionConfig;
117
118 ScriptTester::DiffCommand diff;
119
120 QString preamble;
121 QStringList argv;
122
123 struct Variable {
124 QString key;
125 QString value;
126 };
127
128 QList<Variable> variables;
129
131
132 QByteArray xdgDataDirs;
133
134 DualMode dualMode = DualMode::Dual;
135
136 bool showPreamble = false;
137 bool extendedDebug = false;
138 bool restoreXdgDataDirs = false;
139 bool asText = false;
140};
141
142struct TrueColor {
143 char ansi[11];
144 char isBg : 1;
145 char len : 7;
146
147 /**
148 * @return parsed color with \c len == 0 when there is an error.
149 */
150 static TrueColor fromRGB(QStringView color, bool isBg)
151 {
152 auto toHex = [](QChar c) {
153 if (c <= u'9' && c >= u'0') {
154 return c.unicode() - u'0';
155 }
156 if (c <= u'f' && c >= u'a') {
157 return c.unicode() - u'a';
158 }
159 if (c <= u'F' && c >= u'A') {
160 return c.unicode() - u'A';
161 }
162 return 0;
163 };
164
165 TrueColor trueColor;
166
167 int r = 0;
168 int g = 0;
169 int b = 0;
170 // format: #rgb
171 if (color.size() == 4) {
172 r = toHex(color[1]);
173 g = toHex(color[2]);
174 b = toHex(color[3]);
175 r = (r << 4) + r;
176 g = (g << 4) + g;
177 b = (b << 4) + b;
178 }
179 // format: #rrggbb
180 else if (color.size() == 7) {
181 r = (toHex(color[1]) << 4) + toHex(color[2]);
182 g = (toHex(color[3]) << 4) + toHex(color[4]);
183 b = (toHex(color[5]) << 4) + toHex(color[6]);
184 }
185 // invalid format
186 else {
187 trueColor.len = 0;
188 return trueColor;
189 }
190
191 auto *p = trueColor.ansi;
192 auto pushComponent = [&](int color) {
193 if (color > 99) {
194 *p++ = "0123456789"[color / 100];
195 color /= 10;
196 *p++ = "0123456789"[color / 10];
197 } else if (color > 9) {
198 *p++ = "0123456789"[color / 10];
199 }
200 *p++ = "0123456789"[color % 10];
201 };
202 pushComponent(r);
203 *p++ = ';';
204 pushComponent(g);
205 *p++ = ';';
206 pushComponent(b);
207 trueColor.len = p - trueColor.ansi;
208 trueColor.isBg = isBg;
209
210 return trueColor;
211 }
212
213 QLatin1StringView sv() const
214 {
215 return QLatin1StringView{ansi, len};
216 }
217};
218
219/**
220 * Parse a comma-separated list of color, style, ansi-sequence.
221 * @param str string to parse
222 * @param defaultColor default colors and styles
223 * @param ok failure is reported by setting *ok to false
224 * @return ansi sequence
225 */
226QString toANSIColor(QStringView str, const QString &defaultColor, bool *ok)
227{
228 QString result;
230
231 if (!str.isEmpty()) {
232 qsizetype totalLen = 0;
233 bool hasDefaultColor = !defaultColor.isEmpty();
234 auto colors = str.split(u',', Qt::SkipEmptyParts);
235 if (colors.isEmpty()) {
236 result = defaultColor;
237 return result;
238 }
239
240 /*
241 * Parse colors.
242 * TrueColor are replaced by empty QStringViews and pushed to the trueColors array.
243 */
244 for (auto &color : colors) {
245 // ansi code
246 if (color[0] <= u'9' && color[0] >= u'0') {
247 // check ansi sequence
248 for (auto c : color) {
249 if (c > u'9' && c < u'0' && c != u';') {
250 *ok = false;
251 return result;
252 }
253 }
254 } else {
255 const bool isBg = color.startsWith(u"bg=");
256 auto s = isBg ? color.sliced(3) : color;
257 const bool isBright = s.startsWith(u"bright-");
258 s = isBright ? s.sliced(7) : s;
259 using SVs = const QStringView[];
260
261 // true color
262 if (s[0] == u'#') {
263 if (isBright) {
264 *ok = false;
265 return result;
266 }
267 const auto trueColor = TrueColor::fromRGB(s, isBg);
268 if (!trueColor.len) {
269 *ok = false;
270 return result;
271 }
272 totalLen += 5 + trueColor.len;
273 trueColors += trueColor;
274 color = QStringView();
275 // colors
276 } else if (s == u"black"_sv) {
277 color = SVs{u"30"_sv, u"40"_sv, u"90"_sv, u"100"_sv}[isBg + isBright * 2];
278 } else if (s == u"red"_sv) {
279 color = SVs{u"31"_sv, u"41"_sv, u"91"_sv, u"101"_sv}[isBg + isBright * 2];
280 } else if (s == u"green"_sv) {
281 color = SVs{u"32"_sv, u"42"_sv, u"92"_sv, u"102"_sv}[isBg + isBright * 2];
282 } else if (s == u"yellow"_sv) {
283 color = SVs{u"33"_sv, u"43"_sv, u"93"_sv, u"103"_sv}[isBg + isBright * 2];
284 } else if (s == u"blue"_sv) {
285 color = SVs{u"34"_sv, u"44"_sv, u"94"_sv, u"104"_sv}[isBg + isBright * 2];
286 } else if (s == u"magenta"_sv) {
287 color = SVs{u"35"_sv, u"45"_sv, u"95"_sv, u"105"_sv}[isBg + isBright * 2];
288 } else if (s == u"cyan"_sv) {
289 color = SVs{u"36"_sv, u"46"_sv, u"96"_sv, u"106"_sv}[isBg + isBright * 2];
290 } else if (s == u"white"_sv) {
291 color = SVs{u"37"_sv, u"47"_sv, u"97"_sv, u"107"_sv}[isBg + isBright * 2];
292 // styles
293 } else if (!isBg && !isBright && s == u"bold"_sv) {
294 color = u"1"_sv;
295 } else if (!isBg && !isBright && s == u"dim"_sv) {
296 color = u"2"_sv;
297 } else if (!isBg && !isBright && s == u"italic"_sv) {
298 color = u"3"_sv;
299 } else if (!isBg && !isBright && s == u"underline"_sv) {
300 color = u"4"_sv;
301 } else if (!isBg && !isBright && s == u"reverse"_sv) {
302 color = u"7"_sv;
303 } else if (!isBg && !isBright && s == u"strike"_sv) {
304 color = u"9"_sv;
305 } else if (!isBg && !isBright && s == u"doubly-underlined"_sv) {
306 color = u"21"_sv;
307 } else if (!isBg && !isBright && s == u"overlined"_sv) {
308 color = u"53"_sv;
309 // error
310 } else {
311 *ok = false;
312 return result;
313 }
314 }
315
316 totalLen += color.size() + 1;
317 }
318
319 if (hasDefaultColor) {
320 totalLen += defaultColor.size() - 2;
321 }
322
323 result.reserve(totalLen + 2);
324
325 if (!hasDefaultColor) {
326 result += u"\x1b["_sv;
327 } else {
328 result += defaultColor;
329 result.back() = u';';
330 }
331
332 /*
333 * Concat colors to result
334 */
335 auto const *trueColorIt = trueColors.constData();
336 for (const auto &color : std::as_const(colors)) {
337 if (!color.isEmpty()) {
338 result += color;
339 } else {
340 result += trueColorIt->isBg ? u"48;2;"_sv : u"38;2;"_sv;
341 result += trueColorIt->sv();
342 ++trueColorIt;
343 }
344 result += u';';
345 }
346
347 result.back() = u'm';
348 } else {
349 result = defaultColor;
350 }
351
352 return result;
353}
354
355void initCommandLineParser(QCoreApplication &app, QCommandLineParser &parser)
356{
357 auto tr = [&app](char const *s) {
358 return app.translate("KateScriptTester", s);
359 };
360
361 const auto translatedFolder = tr("folder");
362 const auto translatedOption = tr("option");
363 const auto translatedPattern = tr("pattern");
364 const auto translatedPlaceholder = tr("character");
365 const auto translatedColors = tr("colors");
366
367 parser.setApplicationDescription(tr("Command line utility for testing Kate's command scripts."));
368 parser.addPositionalArgument(tr("file.js"), tr("Test files to run. If file.js represents a folder, this is equivalent to `path/*.js`."), tr("file.js..."));
369
370 parser.addOptions({
371 // input
372 // @{
373 {{u"t"_s, u"text"_s}, tr("Files are treated as javascript code rather than file names.")},
374 // @}
375
376 // error
377 // @{
378 {{u"e"_s, u"max-error"_s}, tr("Maximum number of tests that can fail before stopping.")},
379 {u"q"_s, tr("Alias of --max-error=1.")},
380 {{u"E"_s, u"expected-failure-as-failure"_s}, tr("functions xcmd() and xtest() will always fail.")},
381 // @}
382
383 // paths
384 // @{
385 {{u"s"_s, u"script"_s},
386 tr("Shorcut for --command=${script}/commands --command=${script}/indentation --library=${script}/library --file=${script}/files."),
387 translatedFolder},
388 {{u"c"_s, u"command"_s}, tr("Adds a search folder for loadScript()."), translatedFolder},
389 {{u"l"_s, u"library"_s}, tr("Adds a search folder for require() (KTextEditor JS API)."), translatedFolder},
390 {{u"r"_s, u"file"_s}, tr("Adds a search folder for read() (KTextEditor JS API)."), translatedFolder},
391 {{u"m"_s, u"module"_s}, tr("Adds a search folder for loadModule()."), translatedFolder},
392 {{u"I"_s, u"indent-data-test"_s}, tr("Set indentation base directory for indentFiles()."), translatedFolder},
393 // @}
394
395 // diff command
396 //@{
397 {{u"D"_s, u"diff-path"_s}, tr("Path of diff command."), tr("path")},
398 {{u"A"_s, u"diff-arg"_s}, tr("Argument for diff command. Call this option several times to set multiple parameters."), translatedOption},
399 //@}
400
401 // output format
402 //@{
403 {{u"d"_s, u"debug"_s},
404 tr("Concerning the display of the debug() function. Can be used multiple times to change multiple options.\n"
405 "- location: displays the file and line number of the call (enabled by default)\n"
406 "- function: displays the name of the function that uses debug() (enabled by default)\n"
407 "- stacktrace: show the call stack after the debug message\n"
408 "- flush: debug messages are normally buffered and only displayed in case of error. This option removes buffering\n"
409 "- extended: debug() can take several parameters of various types such as Array or Object. This behavior is specific and should not be exploited "
410 "in final code\n"
411 "- no-location: inverse of location\n"
412 "- no-function: inverse of function\n"
413 "- no-stacktrace: inverse of stacktrace\n"
414 "- no-flush: inverse of flush\n"
415 "- all: enable all\n"
416 "- none: disable all"),
417 translatedOption},
418 {{u"H"_s, u"hidden-name"_s}, tr("Do not display test names.")},
419 {{u"p"_s, u"parade"_s}, tr("Displays all tests run or skipped. By default, only error tests are displayed.")},
420 {{u"V"_s, u"verbose"_s}, tr("Displays input and ouput on each tests. By default, only error tests are displayed.")},
421 {{u"f"_s, u"format"_s},
422 tr("Defines the document text display format:\n"
423 "- raw: no transformation\n"
424 "- js: display in literal string in javascript format\n"
425 "- literal: replaces new lines and tabs with \\n and \\t (default)\n"
426 "- placeholder: replaces new lines and tabs with placeholders specified by --newline and --tab\n"
427 "- placeholder2: replaces tabs with the placeholder specified by --tab\n"),
428 translatedOption},
429 {{u"F"_s, u"block-format"_s}, tr("Same as --format, but with block selection text."), translatedOption},
430 //@}
431
432 // filter
433 //@{
434 {{u"k"_s, u"filter"_s}, tr("Only runs tests whose name matches a regular expression."), translatedPattern},
435 {u"K"_s, tr("Only runs tests whose name does not matches a regular expression."), translatedPattern},
436 //@}
437
438 // placeholders
439 //@{
440 {{u"T"_s, u"tab"_s},
441 tr("Character used to replace a tab in the test display with --format=placeholder. If 2 characters are given, the second corresponds the last "
442 "character replaced. --tab='->' with tabWith=4 gives '--->'."),
443 translatedPlaceholder},
444 {{u"N"_s, u"nl"_s, u"newline"_s}, tr("Character used to replace a new line in the test display with --format=placeholder."), translatedPlaceholder},
445 {{u"P"_s, u"placeholders"_s},
446 tr("Characters used to represent cursors or selections when the test does not specify any, or when the same character represents more than one thing. "
447 "In order:\n"
448 "- cursor\n"
449 "- selection start\n"
450 "- selection end\n"
451 "- secondary cursor\n"
452 "- secondary selection start\n"
453 "- secondary selection end\n"
454 "- virtual text"),
455 tr("symbols")},
456 //@}
457
458 // setup
459 //@{
460 {{u"b"_s, u"dual"_s},
461 tr("Change DUAL_MODE and ALWAYS_DUAL_MODE constants behavior:\n"
462 "- noblock: never block selection (equivalent to setConfig({blockSelection=0}))\n"
463 "- block: always block selection (equivalent to setConfig({blockSelection=1}))\n"
464 "- always-dual: DUAL_MODE = ALWAYS_DUAL_MODE\n"
465 "- no-always-dual: ALWAYS_DUAL_MODE = DUAL_MODE\n"
466 "- dual: default behavior"),
467 tr("arg")},
468
469 {u"B"_s, tr("Alias of --dual=noblock.")},
470
471 {u"arg"_s, tr("Argument add to 'argv' variable in test scripts. Call this option several times to set multiple parameters."), tr("arg")},
472
473 {u"preamble"_s,
474 tr("Uses a different preamble than the default. The result must be a function whose first parameter is the global environment, second is 'argv' array "
475 "and 'this' refers to the internal object.\n"
476 "The {CODE} substring will be replaced by the test code."),
477 tr("js-source")},
478
479 {u"print-preamble"_s, tr("Show preamble.")},
480
481 {u"x"_s,
482 tr("To ensure that tests are not disrupted by system files, the XDG_DATA_DIRS environment variable is replaced by a non-existent folder.\n"
483 "Unfortunately, indentation files are not accessible with indentFiles(), nor are syntax files, according to the KSyntaxHighlighting compilation "
484 "method.\n"
485 "This option cancels the value replacement.")},
486
487 {u"X"_s, tr("force a value for XDG_DATA_DIRS and ignore -x."), tr("path")},
488
489 {{u"S"_s, u"set-variable"_s},
490 tr("Set document variables before running a test file. This is equivalent to `document.setVariable(key, value)` at the start of the file. Call this "
491 "option several times to set multiple parameters."),
492 tr("key=var")},
493 //@}
494
495 // color parameters
496 //@{
497 {u"no-color"_s, tr("No color on the output")},
498
499 {u"color-reset"_s, tr("Sequence to reset color and style."), translatedColors},
500 {u"color-success"_s, tr("Color for success."), translatedColors},
501 {u"color-error"_s, tr("Color for error or exception."), translatedColors},
502 {u"color-carret"_s, tr("Color for '^~~' under error position."), translatedColors},
503 {u"color-debug-marker"_s, tr("Color for 'DEBUG:' and 'PRINT:' prefixes inserted with debug(), print() and printSep()."), translatedColors},
504 {u"color-debug-message"_s, tr("Color for message with debug()."), translatedColors},
505 {u"color-test-name"_s, tr("Color for name of the test."), translatedColors},
506 {u"color-program"_s, tr("Color for program paramater in cmd() / test() and function name in stacktrace."), translatedColors},
507 {u"color-file"_s, tr("Color for file name."), translatedColors},
508 {u"color-line"_s, tr("Color for line number."), translatedColors},
509 {u"color-block-selection-info"_s, tr("Color for [blockSelection=...] in a check."), translatedColors},
510 {u"color-label-info"_s, tr("Color for 'input', 'output', 'result' label when it is displayed as information and not as an error."), translatedColors},
511 {u"color-cursor"_s, tr("Color for cursor placeholder."), translatedColors},
512 {u"color-selection"_s, tr("Color for selection placeholder."), translatedColors},
513 {u"color-secondary-cursor"_s, tr("Color for secondary cursor placeholder."), translatedColors},
514 {u"color-secondary-selection"_s, tr("Color for secondary selection placeholder."), translatedColors},
515 {u"color-block-selection"_s, tr("Color for block selection placeholder."), translatedColors},
516 {u"color-in-selection"_s, tr("Style added for text inside a selection."), translatedColors},
517 {u"color-virtual-text"_s, tr("Color for virtual text placeholder."), translatedColors},
518 {u"color-replacement"_s, tr("Color for text replaced by --format=placeholder."), translatedColors},
519 {u"color-text-result"_s, tr("Color for text representing the inputs and outputs."), translatedColors},
520 {u"color-result"_s,
521 tr("Color added to all colors used to display a result:\n"
522 "--color-cursor\n"
523 "--color-selection\n"
524 "--color-secondary-cursor\n"
525 "--color-secondary-selection\n"
526 "--color-block-selection\n"
527 "--color-virtual-text\n"
528 "--color-replacement\n"
529 "--color-text-result."),
530 translatedColors},
531 //@}
532 });
533}
534
535struct CommandLineParseResult {
536 enum class Status { Ok, Error, VersionRequested, HelpRequested };
537 Status statusCode = Status::Ok;
538 QString errorString = {};
539};
540
541CommandLineParseResult parseCommandLine(QCommandLineParser &parser, ScriptTesterQuery *query)
542{
543 using Status = CommandLineParseResult::Status;
544
545 const QCommandLineOption helpOption = parser.addHelpOption();
546 const QCommandLineOption versionOption = parser.addVersionOption();
547
548 if (!parser.parse(QCoreApplication::arguments()))
549 return {Status::Error, parser.errorText()};
550
551 if (parser.isSet(u"v"_s))
552 return {Status::VersionRequested};
553
554 if (parser.isSet(u"h"_s))
555 return {Status::HelpRequested};
556
557 query->asText = parser.isSet(u"t"_s);
558
559 if (parser.isSet(u"q"_s)) {
560 query->executionConfig.maxError = 1;
561 }
562 if (parser.isSet(u"e"_s)) {
563 bool ok = true;
564 query->executionConfig.maxError = parser.value(u"e"_s).toInt(&ok);
565 if (!ok) {
566 return {Status::Error, u"--max-error: invalid number"_s};
567 }
568 }
569 query->executionConfig.xCheckAsFailure = parser.isSet(u"E"_s);
570
571 if (parser.isSet(u"s"_s)) {
572 auto addPath = [](QStringList &l, QString path) {
573 if (QFile::exists(path)) {
574 l.append(path);
575 }
576 };
577 const auto paths = parser.values(u"s"_s);
578 for (const auto &path : paths) {
579 addPath(query->paths.scripts, path + u"/command"_sv);
580 addPath(query->paths.scripts, path + u"/indentation"_sv);
581 addPath(query->paths.libraries, path + u"/library"_sv);
582 addPath(query->paths.files, path + u"/files"_sv);
583 }
584 }
585
586 auto setPaths = [&parser](QStringList &l, QString opt) {
587 if (parser.isSet(opt)) {
588 const auto paths = parser.values(opt);
589 for (const auto &path : paths) {
590 l.append(path);
591 }
592 }
593 };
594
595 setPaths(query->paths.scripts, u"c"_s);
596 setPaths(query->paths.libraries, u"l"_s);
597 setPaths(query->paths.files, u"r"_s);
598 setPaths(query->paths.modules, u"m"_s);
599 query->paths.indentBaseDir = parser.value(u"I"_s);
600
601 if (parser.isSet(u"d"_s)) {
602 const auto value = parser.value(u"d"_s);
603 if (value == u"location"_sv) {
604 query->format.debugOptions |= DebugOption::WriteLocation;
605 } else if (value == u"function"_sv) {
606 query->format.debugOptions |= DebugOption::WriteFunction;
607 } else if (value == u"stacktrace"_sv) {
608 query->format.debugOptions |= DebugOption::WriteStackTrace;
609 } else if (value == u"flush"_sv) {
610 query->format.debugOptions |= DebugOption::ForceFlush;
611 } else if (value == u"extended"_sv) {
612 query->extendedDebug = true;
613 } else if (value == u"no-location"_sv) {
614 query->format.debugOptions.setFlag(DebugOption::WriteLocation, false);
615 } else if (value == u"no-function"_sv) {
616 query->format.debugOptions.setFlag(DebugOption::WriteFunction, false);
617 } else if (value == u"no-stacktrace"_sv) {
618 query->format.debugOptions.setFlag(DebugOption::WriteStackTrace, false);
619 } else if (value == u"no-flush"_sv) {
620 query->format.debugOptions.setFlag(DebugOption::ForceFlush, false);
621 } else if (value == u"no-extended"_sv) {
622 query->extendedDebug = false;
623 } else if (value == u"all"_sv) {
624 query->extendedDebug = true;
625 query->format.debugOptions = DebugOption::WriteLocation | DebugOption::WriteFunction | DebugOption::WriteStackTrace | DebugOption::ForceFlush;
626 } else if (value == u"none"_sv) {
627 query->extendedDebug = false;
628 query->format.debugOptions = {};
629 } else {
630 return {Status::Error, u"--debug: invalid value"_s};
631 }
632 }
633
634 if (parser.isSet(u"H"_s)) {
635 query->format.testFormatOptions |= TestFormatOption::HiddenTestName;
636 }
637 if (parser.isSet(u"p"_s)) {
638 query->format.testFormatOptions |= TestFormatOption::AlwaysWriteLocation;
639 }
640 if (parser.isSet(u"V"_s)) {
641 query->format.testFormatOptions |= TestFormatOption::AlwaysWriteInputOutput;
642 }
643
644 auto setFormat = [&parser](TextFormat &textFormat, const QString &opt) {
645 if (parser.isSet(opt)) {
646 const auto value = parser.value(opt);
647 if (value == u"raw"_sv) {
648 textFormat = TextFormat::Raw;
649 } else if (value == u"js"_sv) {
650 textFormat = TextFormat::EscapeForDoubleQuote;
651 } else if (value == u"placeholder"_sv) {
652 textFormat = TextFormat::ReplaceNewLineAndTabWithPlaceholder;
653 } else if (value == u"placeholder2"_sv) {
654 textFormat = TextFormat::ReplaceTabWithPlaceholder;
655 } else if (value == u"literal"_sv) {
656 textFormat = TextFormat::ReplaceNewLineAndTabWithLiteral;
657 } else {
658 return false;
659 }
660 }
661 return true;
662 };
663 if (!setFormat(query->format.documentTextFormat, u"f"_s)) {
664 return {Status::Error, u"--format: invalid value"_s};
665 }
666 if (!setFormat(query->format.documentTextFormatWithBlockSelection, u"F"_s)) {
667 return {Status::Error, u"--block-format: invalid value"_s};
668 }
669
670 auto setPattern = [&parser, &query](QString opt, PatternType patternType) {
671 if (parser.isSet(opt)) {
673 query->executionConfig.pattern.setPattern(parser.value(opt));
674 if (!query->executionConfig.pattern.isValid()) {
675 return false;
676 }
677 query->executionConfig.patternType = patternType;
678 }
679 return true;
680 };
681
682 if (!setPattern(u"k"_s, PatternType::Include)) {
683 return {Status::Error, u"-k: "_sv + query->executionConfig.pattern.errorString()};
684 }
685 if (!setPattern(u"K"_s, PatternType::Exclude)) {
686 return {Status::Error, u"-K: "_sv + query->executionConfig.pattern.errorString()};
687 }
688
689 if (parser.isSet(u"T"_s)) {
690 const auto tab = parser.value(u"T"_s);
691 if (tab.size() == 0) {
692 query->format.textReplacement.tab1 = defaultTextReplacement.tab1;
693 query->format.textReplacement.tab2 = defaultTextReplacement.tab2;
694 } else {
695 query->format.textReplacement.tab1 = tab[0];
696 query->format.textReplacement.tab2 = (tab.size() == 1) ? query->format.textReplacement.tab1 : tab[1];
697 }
698 }
699
700 auto getChar = [](const QString &str, qsizetype i, QChar c = QChar()) {
701 return str.size() > i ? str[i] : c;
702 };
703
704 if (parser.isSet(u"N"_s)) {
705 const auto nl = parser.value(u"N"_s);
706 query->format.textReplacement.newLine = getChar(nl, 0, query->format.textReplacement.newLine);
707 }
708
709 if (parser.isSet(u"P"_s)) {
710 const auto symbols = parser.value(u"P"_s);
711 auto &ph = query->format.fallbackPlaceholders;
712 ph.cursor = getChar(symbols, 0, defaultFallbackPlaceholders.cursor);
713 ph.selectionStart = getChar(symbols, 1, defaultFallbackPlaceholders.selectionStart);
714 ph.selectionEnd = getChar(symbols, 2, defaultFallbackPlaceholders.selectionEnd);
715 ph.secondaryCursor = getChar(symbols, 3, defaultFallbackPlaceholders.secondaryCursor);
716 ph.secondarySelectionStart = getChar(symbols, 4, defaultFallbackPlaceholders.secondarySelectionStart);
717 ph.secondarySelectionEnd = getChar(symbols, 5, defaultFallbackPlaceholders.secondarySelectionEnd);
718 ph.virtualText = getChar(symbols, 6, defaultFallbackPlaceholders.virtualText);
719 }
720
721 if (parser.isSet(u"B"_s)) {
722 query->dualMode = DualMode::NoBlockSelection;
723 }
724
725 if (parser.isSet(u"b"_s)) {
726 const auto mode = parser.value(u"b"_s);
727 if (mode == u"noblock"_sv) {
728 query->dualMode = DualMode::NoBlockSelection;
729 } else if (mode == u"block"_sv) {
730 query->dualMode = DualMode::BlockSelection;
731 } else if (mode == u"always-dual"_sv) {
732 query->dualMode = DualMode::DualIsAlwaysDual;
733 } else if (mode == u"no-always-dual"_sv) {
734 query->dualMode = DualMode::AlwaysDualIsDual;
735 } else if (mode == u"dual"_sv) {
736 query->dualMode = DualMode::Dual;
737 } else {
738 return {Status::Error, u"--dual: invalid value"_s};
739 }
740 query->dualMode = DualMode::NoBlockSelection;
741 }
742
743 query->argv = parser.values(u"arg"_s);
744
745 if (parser.isSet(u"preamble"_s)) {
746 query->preamble = parser.value(u"preamble"_s);
747 }
748
749 query->showPreamble = parser.isSet(u"print-preamble"_s);
750
751 if (parser.isSet(u"X"_s)) {
752 query->xdgDataDirs = parser.value(u"X"_s).toUtf8();
753 query->restoreXdgDataDirs = true;
754 } else {
755 query->restoreXdgDataDirs = parser.isSet(u"x"_s);
756 }
757
758 if (parser.isSet(u"S"_s)) {
759 const auto variables = parser.values(u"S"_s);
760 query->variables.resize(variables.size());
761 auto it = query->variables.begin();
762 for (const auto &kv : variables) {
763 auto pos = QStringView(kv).indexOf(u'=');
764 if (pos >= 0) {
765 it->key = kv.sliced(0, pos);
766 it->value = kv.sliced(pos + 1);
767 } else {
768 it->key = kv;
769 }
770 ++it;
771 }
772 }
773
774 query->diff.path = parser.isSet(u"D"_s) ? parser.value(u"D"_s) : u"diff"_s;
775
776 const bool noColor = parser.isSet(u"no-color"_s);
777
778 if (parser.isSet(u"A"_s)) {
779 query->diff.args = parser.values(u"A"_s);
780 } else {
781 query->diff.args.push_back(u"-u"_s);
782 if (!noColor) {
783 query->diff.args.push_back(u"--color"_s);
784 }
785 }
786
787 if (noColor) {
788 query->format.colors.reset.clear();
789 query->format.colors.success.clear();
790 query->format.colors.error.clear();
791 query->format.colors.carret.clear();
792 query->format.colors.debugMarker.clear();
793 query->format.colors.debugMsg.clear();
794 query->format.colors.testName.clear();
795 query->format.colors.program.clear();
796 query->format.colors.fileName.clear();
797 query->format.colors.lineNumber.clear();
798 query->format.colors.labelInfo.clear();
799 query->format.colors.blockSelectionInfo.clear();
800 query->format.colors.cursor.clear();
801 query->format.colors.selection.clear();
802 query->format.colors.secondaryCursor.clear();
803 query->format.colors.secondarySelection.clear();
804 query->format.colors.blockSelection.clear();
805 query->format.colors.inSelection.clear();
806 query->format.colors.virtualText.clear();
807 query->format.colors.result.clear();
808 query->format.colors.resultReplacement.clear();
809 } else {
810 QString defaultResultColor;
811 QString optWithError;
812 auto setColor = [&](QString &color, QString opt) {
813 if (parser.isSet(opt)) {
814 bool ok = true;
815 color = toANSIColor(parser.value(opt), defaultResultColor, &ok);
816 if (!ok) {
817 optWithError = opt;
818 }
819 return true;
820 }
821 return false;
822 };
823
824 setColor(query->format.colors.reset, u"color-reset"_s);
825 setColor(query->format.colors.success, u"color-success"_s);
826 setColor(query->format.colors.error, u"color-error"_s);
827 setColor(query->format.colors.carret, u"color-carret"_s);
828 setColor(query->format.colors.debugMarker, u"color-debug-marker"_s);
829 setColor(query->format.colors.debugMsg, u"color-debug-message"_s);
830 setColor(query->format.colors.testName, u"color-test-name"_s);
831 setColor(query->format.colors.program, u"color-program"_s);
832 setColor(query->format.colors.fileName, u"color-file"_s);
833 setColor(query->format.colors.lineNumber, u"color-line"_s);
834 setColor(query->format.colors.labelInfo, u"color-label-info"_s);
835 setColor(query->format.colors.blockSelectionInfo, u"color-block-selection-info"_s);
836 setColor(query->format.colors.inSelection, u"color-in-selection"_s);
837
838 if (!setColor(defaultResultColor, u"color-result"_s)) {
839 defaultResultColor = u"\x1b[40m"_s;
840 }
841 const bool hasDefault = defaultResultColor.size();
842 const QStringView ansiBg = QStringView(defaultResultColor.constData(), hasDefault ? defaultResultColor.size() - 1 : 0);
843 if (!setColor(query->format.colors.cursor, u"color-cursor"_s) && hasDefault) {
844 query->format.colors.cursor = ansiBg % u";1;33m"_sv;
845 }
846 if (!setColor(query->format.colors.selection, u"color-selection"_s) && hasDefault) {
847 query->format.colors.selection = ansiBg % u";1;33m"_sv;
848 }
849 if (!setColor(query->format.colors.secondaryCursor, u"color-secondary-cursor"_s) && hasDefault) {
850 query->format.colors.secondaryCursor = ansiBg % u";33m"_sv;
851 }
852 if (!setColor(query->format.colors.secondarySelection, u"color-secondary-selection"_s) && hasDefault) {
853 query->format.colors.secondarySelection = ansiBg % u";33m"_sv;
854 }
855 if (!setColor(query->format.colors.blockSelection, u"color-block-selection"_s) && hasDefault) {
856 query->format.colors.blockSelection = ansiBg % u";37m"_sv;
857 }
858 if (!setColor(query->format.colors.virtualText, u"color-virtual-text"_s) && hasDefault) {
859 query->format.colors.virtualText = ansiBg % u";37m"_sv;
860 }
861 if (!setColor(query->format.colors.result, u"color-text-result"_s) && hasDefault) {
862 query->format.colors.result = defaultResultColor;
863 }
864 if (!setColor(query->format.colors.resultReplacement, u"color-replacement"_s) && hasDefault) {
865 query->format.colors.resultReplacement = ansiBg % u";36m"_sv;
866 }
867
868 if (!optWithError.isEmpty()) {
869 return {Status::Error, u"--"_sv % optWithError % u": invalid color"_sv};
870 }
871 }
872
873 query->fileNames = parser.positionalArguments();
874
875 return {Status::Ok};
876}
877
878void addTextStyleProperties(QJSValue &obj)
879{
881 obj.setProperty(u"dsNormal"_s, TextStyle::Normal);
882 obj.setProperty(u"dsKeyword"_s, TextStyle::Keyword);
883 obj.setProperty(u"dsFunction"_s, TextStyle::Function);
884 obj.setProperty(u"dsVariable"_s, TextStyle::Variable);
885 obj.setProperty(u"dsControlFlow"_s, TextStyle::ControlFlow);
886 obj.setProperty(u"dsOperator"_s, TextStyle::Operator);
887 obj.setProperty(u"dsBuiltIn"_s, TextStyle::BuiltIn);
888 obj.setProperty(u"dsExtension"_s, TextStyle::Extension);
889 obj.setProperty(u"dsPreprocessor"_s, TextStyle::Preprocessor);
890 obj.setProperty(u"dsAttribute"_s, TextStyle::Attribute);
891 obj.setProperty(u"dsChar"_s, TextStyle::Char);
892 obj.setProperty(u"dsSpecialChar"_s, TextStyle::SpecialChar);
893 obj.setProperty(u"dsString"_s, TextStyle::String);
894 obj.setProperty(u"dsVerbatimString"_s, TextStyle::VerbatimString);
895 obj.setProperty(u"dsSpecialString"_s, TextStyle::SpecialString);
896 obj.setProperty(u"dsImport"_s, TextStyle::Import);
897 obj.setProperty(u"dsDataType"_s, TextStyle::DataType);
898 obj.setProperty(u"dsDecVal"_s, TextStyle::DecVal);
899 obj.setProperty(u"dsBaseN"_s, TextStyle::BaseN);
900 obj.setProperty(u"dsFloat"_s, TextStyle::Float);
901 obj.setProperty(u"dsConstant"_s, TextStyle::Constant);
902 obj.setProperty(u"dsComment"_s, TextStyle::Comment);
903 obj.setProperty(u"dsDocumentation"_s, TextStyle::Documentation);
904 obj.setProperty(u"dsAnnotation"_s, TextStyle::Annotation);
905 obj.setProperty(u"dsCommentVar"_s, TextStyle::CommentVar);
906 obj.setProperty(u"dsRegionMarker"_s, TextStyle::RegionMarker);
907 obj.setProperty(u"dsInformation"_s, TextStyle::Information);
908 obj.setProperty(u"dsWarning"_s, TextStyle::Warning);
909 obj.setProperty(u"dsAlert"_s, TextStyle::Alert);
910 obj.setProperty(u"dsOthers"_s, TextStyle::Others);
911 obj.setProperty(u"dsError"_s, TextStyle::Error);
912}
913
914/**
915 * Timestamp in milliseconds.
916 */
917static qsizetype timeNowInMs()
918{
919 auto t = std::chrono::high_resolution_clock::now().time_since_epoch();
920 return std::chrono::duration_cast<std::chrono::milliseconds>(t).count();
921}
922
923QtMessageHandler originalHandler = nullptr;
924/**
925 * Remove messages from kf.sonnet.core when no backend is found.
926 */
927static void filterMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
928{
929 if (originalHandler && context.category != std::string_view("kf.sonnet.core")) {
930 originalHandler(type, context, msg);
931 }
932}
933
934} // anonymous namespace
935
936int main(int ac, char **av)
937{
938 ScriptTesterQuery query;
939 query.xdgDataDirs = qgetenv("XDG_DATA_DIRS");
940
941 qputenv("QT_QPA_PLATFORM", "offscreen"); // equivalent to `-platform offscreen` in cli
942 // Set an unknown folder for XDG_DATA_DIRS so that KateScriptManager::collect()
943 // does not retrieve system scripts.
944 // If the variable is empty, QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)
945 // returns /usr/local/share and /usr/share
946 qputenv("XDG_DATA_DIRS", "/XDG_DATA_DIRS_unknown_folder");
948 originalHandler = qInstallMessageHandler(filterMessageOutput);
949
950 /*
951 * App
952 */
953
954 QApplication app(ac, av);
955 QCoreApplication::setApplicationName(u"katescripttester"_s);
958 QCoreApplication::setApplicationVersion(QStringLiteral(KTEXTEDITOR_VERSION_STRING));
959
960 /*
961 * Cli parser
962 */
963
964 QCommandLineParser parser;
965 initCommandLineParser(app, parser);
966
967 using Status = CommandLineParseResult::Status;
968 CommandLineParseResult parseResult = parseCommandLine(parser, &query);
969 switch (parseResult.statusCode) {
970 case Status::Ok:
971 if (!query.showPreamble && query.fileNames.isEmpty()) {
972 std::fputs("No test file specified.\nUse -h / --help for more details.\n", stderr);
973 return 1;
974 }
975 break;
976 case Status::Error:
977 std::fputs(qPrintable(parseResult.errorString), stderr);
978 std::fputs("\nUse -h / --help for more details.\n", stderr);
979 return 2;
980 case Status::VersionRequested:
981 parser.showVersion();
982 return 0;
983 case Status::HelpRequested:
984 std::fputs(qPrintable(parser.helpText()), stdout);
985 std::fputs(R"(
986Colors:
987 Comma-separated list of values:
988 - color name: black, green, yellow, blue, magenta, cyan, white
989 - bright color name: bright-${color name}
990 - rgb: #fff or #ffffff (use trueColor sequence)
991 - background color: bg=${color name} or bg=bright-${color name} bg=${rgb}
992 - style: bold, dim, italic, underline, reverse, strike, doubly-underline, overlined
993 - ANSI sequence: number sequence with optional ';'
994)",
995 stdout);
996 return 0;
997 }
998
999 /*
1000 * Init Preamble
1001 */
1002
1003 // no new line so that the lines indicated by evaluate correspond to the user code
1004 auto jsInjectionStart1 =
1005 u"(function(env, argv){"
1006 u"const TestFramework = this.loadModule(':/ktexteditor/scripttester/testframework.js');"
1007 u"const {REUSE_LAST_INPUT, REUSE_LAST_EXPECTED_OUTPUT} = TestFramework;"
1008 u"const AS_INPUT = TestFramework.EXPECTED_OUTPUT_AS_INPUT;"
1009 u"var {calleeWrapper, config, print, printSep, testCase, sequence, withInput, keys,"
1010 u" indentFiles, test, xtest, eqvTrue, eqvFalse, eqTrue, eqFalse, error, errorMsg,"
1011 u" errorType, hasError, eqv, is, eq, ne, lt, gt, le, ge, cmd, xcmd, type, xtype"
1012 u" } = TestFramework;"
1013 u"var c = TestFramework.sanitizeTag;"
1014 u"var lazyfn = (fn, ...args) => new TestFramework.LazyFunc(fn, ...args);"
1015 u"var fn = lazyfn;"
1016 u"var lazyarg = (arg) => new TestFramework.LazyArg(arg);"
1017 u"var arg = lazyarg;"
1018 u"var loadScript = this.loadScript;"
1019 u"var loadModule = this.loadModule;"
1020 u"var paste = (str) => this.paste(str);"
1021 u"env.editor = TestFramework.editor;" // init editor
1022 u"var document = calleeWrapper('document', env.document);"
1023 u"var editor = calleeWrapper('editor', env.editor);"
1024 u"var view = calleeWrapper('view', env.view);"
1025 u""_sv;
1026 auto debugSetup = query.extendedDebug ? u"debug = testFramework.debug;"_sv : u""_sv;
1027 // clang-format off
1028 auto dualModeSetup = query.dualMode == DualMode::Dual
1029 ? u"const DUAL_MODE = TestFramework.DUAL_MODE;"
1030 u"const ALWAYS_DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"_sv
1031 : query.dualMode == DualMode::NoBlockSelection
1032 ? u"const DUAL_MODE = 0;"
1033 u"const ALWAYS_DUAL_MODE = 0;"_sv
1034 : query.dualMode == DualMode::BlockSelection
1035 ? u"const DUAL_MODE = 1;"
1036 u"const ALWAYS_DUAL_MODE = 1;"_sv
1037 : query.dualMode == DualMode::DualIsAlwaysDual
1038 ? u"const DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"
1039 u"const ALWAYS_DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"_sv
1040 // : query.dualMode == DualMode::AlwaysDualIsDual
1041 : u"const DUAL_MODE = TestFramework.DUAL_MODE;"
1042 u"const ALWAYS_DUAL_MODE = TestFramework.DUAL_MODE;"_sv;
1043 // clang-format on
1044 auto jsInjectionStart2 =
1045 u"var kbd = TestFramework.init(this, env, DUAL_MODE);"
1046 u"try { void function(){"_sv;
1047 auto jsInjectionEnd =
1048 u"\n}() }"
1049 u"catch (e) {"
1050 u"if (e !== TestFramework.STOP_CASE_ERROR) {"
1051 u"throw e;"
1052 u"}"
1053 u"}"
1054 u"})\n"
1055 u""_sv;
1056
1057 if (!query.preamble.isEmpty()) {
1058 const auto pattern = u"{CODE}"_sv;
1059 const QStringView preamble = query.preamble;
1060 auto pos = preamble.indexOf(pattern);
1061 if (pos <= -1) {
1062 std::fputs("missing {CODE} with --preamble\n", stderr);
1063 return 2;
1064 }
1065 jsInjectionStart1 = preamble.sliced(0, pos);
1066 jsInjectionEnd = preamble.sliced(pos + pattern.size());
1067 jsInjectionStart2 = QStringView();
1068 dualModeSetup = QStringView();
1069 debugSetup = QStringView();
1070 }
1071
1072 auto makeProgram = [&](QStringView source) -> QString {
1073 return jsInjectionStart1 % debugSetup % dualModeSetup % jsInjectionStart2 % u'\n' % source % jsInjectionEnd;
1074 };
1075
1076 if (query.showPreamble) {
1077 std::fputs(qPrintable(makeProgram(u"{CODE}"_sv)), stdout);
1078 return 0;
1079 }
1080
1081 if (query.restoreXdgDataDirs) {
1082 qputenv("XDG_DATA_DIRS", query.xdgDataDirs);
1083 }
1084
1085 /*
1086 * KTextEditor objects
1087 */
1088
1089 KTextEditor::DocumentPrivate doc(true, false);
1090 KTextEditor::ViewPrivate view(&doc, nullptr);
1091
1092 QJSEngine engine;
1093
1094 KateScriptView viewObj(&engine);
1095 viewObj.setView(&view);
1096
1097 KateScriptDocument docObj(&engine);
1098 docObj.setDocument(&doc);
1099
1100 /*
1101 * ScriptTester object
1102 */
1103
1104 QFile output;
1105 output.open(stderr, QIODevice::WriteOnly);
1106 ScriptTester scriptTester(&output, query.format, query.paths, query.executionConfig, query.diff, defaultPlaceholder, &engine, &doc, &view);
1107
1108 /*
1109 * JS API
1110 */
1111
1112 QJSValue globalObject = engine.globalObject();
1113 QJSValue functions = engine.newQObject(&scriptTester);
1114
1115 globalObject.setProperty(u"read"_s, functions.property(u"read"_s));
1116 globalObject.setProperty(u"require"_s, functions.property(u"require"_s));
1117 globalObject.setProperty(u"debug"_s, functions.property(u"debug"_s));
1118
1119 globalObject.setProperty(u"view"_s, engine.newQObject(&viewObj));
1120 globalObject.setProperty(u"document"_s, engine.newQObject(&docObj));
1121 // editor object is defined later in testframwork.js
1122
1123 addTextStyleProperties(globalObject);
1124
1125 // View and Document expose JS Range objects in the API, which will fail to work
1126 // if Range is not included. range.js includes cursor.js
1127 scriptTester.require(u"range.js"_s);
1128
1129 engine.evaluate(QStringLiteral(
1130 // translation functions (return untranslated text)
1131 "function i18n(text, ...arg) { return text; }\n"
1132 "function i18nc(context, text, ...arg) { return text; }\n"
1133 "function i18np(singular, plural, number, ...arg) { return number > 1 ? plural : singular; }\n"
1134 "function i18ncp(context, singular, plural, number, ...arg) { return number > 1 ? plural : singular; }\n"
1135 // editor object, defined in testframwork.js and built before running a test
1136 "var editor = undefined;"));
1137
1138 /*
1139 * Run function
1140 */
1141
1142 auto jsArgv = engine.newArray(query.argv.size());
1143 for (quint32 i = 0; i < query.argv.size(); ++i) {
1144 jsArgv.setProperty(i, QJSValue(query.argv.constData()[i]));
1145 }
1146
1147 const auto &colors = query.format.colors;
1148
1149 qsizetype delayInMs = 0;
1150 bool resetConfig = false;
1151 auto runProgram = [&](const QString &fileName, const QString &source) {
1152 auto result = engine.evaluate(makeProgram(source), fileName, 0);
1153 if (!result.isError()) {
1154 if (resetConfig) {
1155 scriptTester.resetConfig();
1156 }
1157 resetConfig = true;
1158
1159 for (const auto &variable : std::as_const(query.variables)) {
1160 doc.setVariable(variable.key, variable.value);
1161 }
1162
1163 const auto start = timeNowInMs();
1164 result = result.callWithInstance(functions, {globalObject, jsArgv});
1165 delayInMs += timeNowInMs() - start;
1166 if (!result.isError()) {
1167 return;
1168 }
1169 }
1170
1171 scriptTester.incrementError();
1172 scriptTester.stream() << colors.error << result.toString() << colors.reset << u'\n';
1173 scriptTester.writeException(result, u"| "_sv);
1174 scriptTester.stream().flush();
1175 };
1176
1177 QFile file;
1178 auto runJsFile = [&](const QString &fileName) {
1179 file.setFileName(fileName);
1181 const QString content = ok ? QTextStream(&file).readAll() : QString();
1182 ok = (ok && file.error() == QFileDevice::NoError);
1183 if (!ok) {
1184 scriptTester.incrementError();
1185 scriptTester.stream() << colors.fileName << fileName << colors.reset << ": "_L1 << colors.error << file.errorString() << colors.reset << u'\n';
1186 scriptTester.stream().flush();
1187 }
1188 file.close();
1189 file.unsetError();
1190 if (ok) {
1191 runProgram(fileName, content);
1192 }
1193 };
1194
1195 /*
1196 * Read file and run
1197 */
1198
1199 const auto &fileNames = query.fileNames;
1200 for (const auto &fileName : fileNames) {
1201 if (query.asText) {
1202 runProgram(u"file%1.js"_s.arg(&fileName - fileNames.data() + 1), fileName);
1203 } else if (!QFileInfo(fileName).isDir()) {
1204 runJsFile(fileName);
1205 } else {
1206 QDirIterator it(fileName, {u"*.js"_s}, QDir::Files);
1207 while (it.hasNext() && !scriptTester.hasTooManyErrors()) {
1208 runJsFile(it.next());
1209 }
1210 }
1211
1212 if (scriptTester.hasTooManyErrors()) {
1213 break;
1214 }
1215 }
1216
1217 /*
1218 * Result
1219 */
1220
1221 if (scriptTester.hasTooManyErrors()) {
1222 scriptTester.stream() << colors.error << "Too many error"_L1 << colors.reset << u'\n';
1223 }
1224
1225 scriptTester.writeSummary();
1226 scriptTester.stream() << " Duration: "_L1 << delayInMs << "ms\n"_L1;
1227 scriptTester.stream().flush();
1228
1229 return scriptTester.countError() ? 1 : 0;
1230}
Backend of KTextEditor::Document related public KTextEditor interfaces.
Thinish wrapping around KTextEditor::DocumentPrivate, exposing the methods we want exposed and adding...
Thinish wrapping around KTextEditor::ViewPrivate, exposing the methods we want exposed and adding som...
Q_SCRIPTABLE Q_NOREPLY void start()
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QString path(const QString &relativePath)
KEDUVOCDOCUMENT_EXPORT QStringList fileNames(const QString &language=QString())
QCommandLineOption addHelpOption()
bool addOptions(const QList< QCommandLineOption > &options)
void addPositionalArgument(const QString &name, const QString &description, const QString &syntax)
QCommandLineOption addVersionOption()
QString errorText() const const
QString helpText() const const
bool isSet(const QCommandLineOption &option) const const
bool parse(const QStringList &arguments)
QStringList positionalArguments() const const
void setApplicationDescription(const QString &description)
QString value(const QCommandLineOption &option) const const
QStringList values(const QCommandLineOption &option) const const
void setApplicationName(const QString &application)
void setApplicationVersion(const QString &version)
QStringList arguments()
void setOrganizationDomain(const QString &orgDomain)
void setOrganizationName(const QString &orgName)
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
virtual void close() override
FileError error() const const
void unsetError()
QString errorString() const const
QJSValue evaluate(const QString &program, const QString &fileName, int lineNumber, QStringList *exceptionStackTrace)
QJSValue globalObject() const const
QJSValue newArray(uint length)
QJSValue newQObject(QObject *object)
QJSValue property(const QString &name) const const
void setProperty(const QString &name, const QJSValue &value)
void append(QList< T > &&value)
iterator begin()
void clear()
const_pointer constData() const const
pointer data()
bool isEmpty() const const
void push_back(parameter_type value)
void resize(qsizetype size)
qsizetype size() const const
void setTestModeEnabled(bool testMode)
QChar & back()
const QChar * constData() const const
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
int toInt(bool *ok, int base) const const
QByteArray toUtf8() const const
qsizetype indexOf(QChar c, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype size() const const
QStringView sliced(qsizetype pos) const const
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
SkipEmptyParts
TextFormat
QString readAll()
const T * constData() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Aug 30 2024 11:52:24 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.