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

KDE's Doxygen guidelines are available online.