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 {
537 Ok,
538 Error,
539 VersionRequested,
540 HelpRequested
541 };
542 Status statusCode = Status::Ok;
543 QString errorString = {};
544};
545
546CommandLineParseResult parseCommandLine(QCommandLineParser &parser, ScriptTesterQuery *query)
547{
548 using Status = CommandLineParseResult::Status;
549
550 const QCommandLineOption helpOption = parser.addHelpOption();
551 const QCommandLineOption versionOption = parser.addVersionOption();
552
553 if (!parser.parse(QCoreApplication::arguments()))
554 return {Status::Error, parser.errorText()};
555
556 if (parser.isSet(u"v"_s))
557 return {Status::VersionRequested};
558
559 if (parser.isSet(u"h"_s))
560 return {Status::HelpRequested};
561
562 query->asText = parser.isSet(u"t"_s);
563
564 if (parser.isSet(u"q"_s)) {
565 query->executionConfig.maxError = 1;
566 }
567 if (parser.isSet(u"e"_s)) {
568 bool ok = true;
569 query->executionConfig.maxError = parser.value(u"e"_s).toInt(&ok);
570 if (!ok) {
571 return {Status::Error, u"--max-error: invalid number"_s};
572 }
573 }
574 query->executionConfig.xCheckAsFailure = parser.isSet(u"E"_s);
575
576 if (parser.isSet(u"s"_s)) {
577 auto addPath = [](QStringList &l, QString path) {
578 if (QFile::exists(path)) {
579 l.append(path);
580 }
581 };
582 const auto paths = parser.values(u"s"_s);
583 for (const auto &path : paths) {
584 addPath(query->paths.scripts, path + u"/command"_sv);
585 addPath(query->paths.scripts, path + u"/indentation"_sv);
586 addPath(query->paths.libraries, path + u"/library"_sv);
587 addPath(query->paths.files, path + u"/files"_sv);
588 }
589 }
590
591 auto setPaths = [&parser](QStringList &l, QString opt) {
592 if (parser.isSet(opt)) {
593 const auto paths = parser.values(opt);
594 for (const auto &path : paths) {
595 l.append(path);
596 }
597 }
598 };
599
600 setPaths(query->paths.scripts, u"c"_s);
601 setPaths(query->paths.libraries, u"l"_s);
602 setPaths(query->paths.files, u"r"_s);
603 setPaths(query->paths.modules, u"m"_s);
604 query->paths.indentBaseDir = parser.value(u"I"_s);
605
606 if (parser.isSet(u"d"_s)) {
607 const auto value = parser.value(u"d"_s);
608 if (value == u"location"_sv) {
609 query->format.debugOptions |= DebugOption::WriteLocation;
610 } else if (value == u"function"_sv) {
611 query->format.debugOptions |= DebugOption::WriteFunction;
612 } else if (value == u"stacktrace"_sv) {
613 query->format.debugOptions |= DebugOption::WriteStackTrace;
614 } else if (value == u"flush"_sv) {
615 query->format.debugOptions |= DebugOption::ForceFlush;
616 } else if (value == u"extended"_sv) {
617 query->extendedDebug = true;
618 } else if (value == u"no-location"_sv) {
619 query->format.debugOptions.setFlag(DebugOption::WriteLocation, false);
620 } else if (value == u"no-function"_sv) {
621 query->format.debugOptions.setFlag(DebugOption::WriteFunction, false);
622 } else if (value == u"no-stacktrace"_sv) {
623 query->format.debugOptions.setFlag(DebugOption::WriteStackTrace, false);
624 } else if (value == u"no-flush"_sv) {
625 query->format.debugOptions.setFlag(DebugOption::ForceFlush, false);
626 } else if (value == u"no-extended"_sv) {
627 query->extendedDebug = false;
628 } else if (value == u"all"_sv) {
629 query->extendedDebug = true;
630 query->format.debugOptions = DebugOption::WriteLocation | DebugOption::WriteFunction | DebugOption::WriteStackTrace | DebugOption::ForceFlush;
631 } else if (value == u"none"_sv) {
632 query->extendedDebug = false;
633 query->format.debugOptions = {};
634 } else {
635 return {Status::Error, u"--debug: invalid value"_s};
636 }
637 }
638
639 if (parser.isSet(u"H"_s)) {
640 query->format.testFormatOptions |= TestFormatOption::HiddenTestName;
641 }
642 if (parser.isSet(u"p"_s)) {
643 query->format.testFormatOptions |= TestFormatOption::AlwaysWriteLocation;
644 }
645 if (parser.isSet(u"V"_s)) {
646 query->format.testFormatOptions |= TestFormatOption::AlwaysWriteInputOutput;
647 }
648
649 auto setFormat = [&parser](TextFormat &textFormat, const QString &opt) {
650 if (parser.isSet(opt)) {
651 const auto value = parser.value(opt);
652 if (value == u"raw"_sv) {
653 textFormat = TextFormat::Raw;
654 } else if (value == u"js"_sv) {
655 textFormat = TextFormat::EscapeForDoubleQuote;
656 } else if (value == u"placeholder"_sv) {
657 textFormat = TextFormat::ReplaceNewLineAndTabWithPlaceholder;
658 } else if (value == u"placeholder2"_sv) {
659 textFormat = TextFormat::ReplaceTabWithPlaceholder;
660 } else if (value == u"literal"_sv) {
661 textFormat = TextFormat::ReplaceNewLineAndTabWithLiteral;
662 } else {
663 return false;
664 }
665 }
666 return true;
667 };
668 if (!setFormat(query->format.documentTextFormat, u"f"_s)) {
669 return {Status::Error, u"--format: invalid value"_s};
670 }
671 if (!setFormat(query->format.documentTextFormatWithBlockSelection, u"F"_s)) {
672 return {Status::Error, u"--block-format: invalid value"_s};
673 }
674
675 auto setPattern = [&parser, &query](QString opt, PatternType patternType) {
676 if (parser.isSet(opt)) {
678 query->executionConfig.pattern.setPattern(parser.value(opt));
679 if (!query->executionConfig.pattern.isValid()) {
680 return false;
681 }
682 query->executionConfig.patternType = patternType;
683 }
684 return true;
685 };
686
687 if (!setPattern(u"k"_s, PatternType::Include)) {
688 return {Status::Error, u"-k: "_sv + query->executionConfig.pattern.errorString()};
689 }
690 if (!setPattern(u"K"_s, PatternType::Exclude)) {
691 return {Status::Error, u"-K: "_sv + query->executionConfig.pattern.errorString()};
692 }
693
694 if (parser.isSet(u"T"_s)) {
695 const auto tab = parser.value(u"T"_s);
696 if (tab.size() == 0) {
697 query->format.textReplacement.tab1 = defaultTextReplacement.tab1;
698 query->format.textReplacement.tab2 = defaultTextReplacement.tab2;
699 } else {
700 query->format.textReplacement.tab1 = tab[0];
701 query->format.textReplacement.tab2 = (tab.size() == 1) ? query->format.textReplacement.tab1 : tab[1];
702 }
703 }
704
705 auto getChar = [](const QString &str, qsizetype i, QChar c = QChar()) {
706 return str.size() > i ? str[i] : c;
707 };
708
709 if (parser.isSet(u"N"_s)) {
710 const auto nl = parser.value(u"N"_s);
711 query->format.textReplacement.newLine = getChar(nl, 0, query->format.textReplacement.newLine);
712 }
713
714 if (parser.isSet(u"P"_s)) {
715 const auto symbols = parser.value(u"P"_s);
716 auto &ph = query->format.fallbackPlaceholders;
717 ph.cursor = getChar(symbols, 0, defaultFallbackPlaceholders.cursor);
718 ph.selectionStart = getChar(symbols, 1, defaultFallbackPlaceholders.selectionStart);
719 ph.selectionEnd = getChar(symbols, 2, defaultFallbackPlaceholders.selectionEnd);
720 ph.secondaryCursor = getChar(symbols, 3, defaultFallbackPlaceholders.secondaryCursor);
721 ph.secondarySelectionStart = getChar(symbols, 4, defaultFallbackPlaceholders.secondarySelectionStart);
722 ph.secondarySelectionEnd = getChar(symbols, 5, defaultFallbackPlaceholders.secondarySelectionEnd);
723 ph.virtualText = getChar(symbols, 6, defaultFallbackPlaceholders.virtualText);
724 }
725
726 if (parser.isSet(u"B"_s)) {
727 query->dualMode = DualMode::NoBlockSelection;
728 }
729
730 if (parser.isSet(u"b"_s)) {
731 const auto mode = parser.value(u"b"_s);
732 if (mode == u"noblock"_sv) {
733 query->dualMode = DualMode::NoBlockSelection;
734 } else if (mode == u"block"_sv) {
735 query->dualMode = DualMode::BlockSelection;
736 } else if (mode == u"always-dual"_sv) {
737 query->dualMode = DualMode::DualIsAlwaysDual;
738 } else if (mode == u"no-always-dual"_sv) {
739 query->dualMode = DualMode::AlwaysDualIsDual;
740 } else if (mode == u"dual"_sv) {
741 query->dualMode = DualMode::Dual;
742 } else {
743 return {Status::Error, u"--dual: invalid value"_s};
744 }
745 query->dualMode = DualMode::NoBlockSelection;
746 }
747
748 query->argv = parser.values(u"arg"_s);
749
750 if (parser.isSet(u"preamble"_s)) {
751 query->preamble = parser.value(u"preamble"_s);
752 }
753
754 query->showPreamble = parser.isSet(u"print-preamble"_s);
755
756 if (parser.isSet(u"X"_s)) {
757 query->xdgDataDirs = parser.value(u"X"_s).toUtf8();
758 query->restoreXdgDataDirs = true;
759 } else {
760 query->restoreXdgDataDirs = parser.isSet(u"x"_s);
761 }
762
763 if (parser.isSet(u"S"_s)) {
764 const auto variables = parser.values(u"S"_s);
765 query->variables.resize(variables.size());
766 auto it = query->variables.begin();
767 for (const auto &kv : variables) {
768 auto pos = QStringView(kv).indexOf(u'=');
769 if (pos >= 0) {
770 it->key = kv.sliced(0, pos);
771 it->value = kv.sliced(pos + 1);
772 } else {
773 it->key = kv;
774 }
775 ++it;
776 }
777 }
778
779 query->diff.path = parser.isSet(u"D"_s) ? parser.value(u"D"_s) : u"diff"_s;
780
781 const bool noColor = parser.isSet(u"no-color"_s);
782
783 if (parser.isSet(u"A"_s)) {
784 query->diff.args = parser.values(u"A"_s);
785 } else {
786 query->diff.args.push_back(u"-u"_s);
787 if (!noColor) {
788 query->diff.args.push_back(u"--color"_s);
789 }
790 }
791
792 if (noColor) {
793 query->format.colors.reset.clear();
794 query->format.colors.success.clear();
795 query->format.colors.error.clear();
796 query->format.colors.carret.clear();
797 query->format.colors.debugMarker.clear();
798 query->format.colors.debugMsg.clear();
799 query->format.colors.testName.clear();
800 query->format.colors.program.clear();
801 query->format.colors.fileName.clear();
802 query->format.colors.lineNumber.clear();
803 query->format.colors.labelInfo.clear();
804 query->format.colors.blockSelectionInfo.clear();
805 query->format.colors.cursor.clear();
806 query->format.colors.selection.clear();
807 query->format.colors.secondaryCursor.clear();
808 query->format.colors.secondarySelection.clear();
809 query->format.colors.blockSelection.clear();
810 query->format.colors.inSelection.clear();
811 query->format.colors.virtualText.clear();
812 query->format.colors.result.clear();
813 query->format.colors.resultReplacement.clear();
814 } else {
815 QString defaultResultColor;
816 QString optWithError;
817 auto setColor = [&](QString &color, QString opt) {
818 if (parser.isSet(opt)) {
819 bool ok = true;
820 color = toANSIColor(parser.value(opt), defaultResultColor, &ok);
821 if (!ok) {
822 optWithError = opt;
823 }
824 return true;
825 }
826 return false;
827 };
828
829 setColor(query->format.colors.reset, u"color-reset"_s);
830 setColor(query->format.colors.success, u"color-success"_s);
831 setColor(query->format.colors.error, u"color-error"_s);
832 setColor(query->format.colors.carret, u"color-carret"_s);
833 setColor(query->format.colors.debugMarker, u"color-debug-marker"_s);
834 setColor(query->format.colors.debugMsg, u"color-debug-message"_s);
835 setColor(query->format.colors.testName, u"color-test-name"_s);
836 setColor(query->format.colors.program, u"color-program"_s);
837 setColor(query->format.colors.fileName, u"color-file"_s);
838 setColor(query->format.colors.lineNumber, u"color-line"_s);
839 setColor(query->format.colors.labelInfo, u"color-label-info"_s);
840 setColor(query->format.colors.blockSelectionInfo, u"color-block-selection-info"_s);
841 setColor(query->format.colors.inSelection, u"color-in-selection"_s);
842
843 if (!setColor(defaultResultColor, u"color-result"_s)) {
844 defaultResultColor = u"\x1b[40m"_s;
845 }
846 const bool hasDefault = defaultResultColor.size();
847 const QStringView ansiBg = QStringView(defaultResultColor.constData(), hasDefault ? defaultResultColor.size() - 1 : 0);
848 if (!setColor(query->format.colors.cursor, u"color-cursor"_s) && hasDefault) {
849 query->format.colors.cursor = ansiBg % u";1;33m"_sv;
850 }
851 if (!setColor(query->format.colors.selection, u"color-selection"_s) && hasDefault) {
852 query->format.colors.selection = ansiBg % u";1;33m"_sv;
853 }
854 if (!setColor(query->format.colors.secondaryCursor, u"color-secondary-cursor"_s) && hasDefault) {
855 query->format.colors.secondaryCursor = ansiBg % u";33m"_sv;
856 }
857 if (!setColor(query->format.colors.secondarySelection, u"color-secondary-selection"_s) && hasDefault) {
858 query->format.colors.secondarySelection = ansiBg % u";33m"_sv;
859 }
860 if (!setColor(query->format.colors.blockSelection, u"color-block-selection"_s) && hasDefault) {
861 query->format.colors.blockSelection = ansiBg % u";37m"_sv;
862 }
863 if (!setColor(query->format.colors.virtualText, u"color-virtual-text"_s) && hasDefault) {
864 query->format.colors.virtualText = ansiBg % u";37m"_sv;
865 }
866 if (!setColor(query->format.colors.result, u"color-text-result"_s) && hasDefault) {
867 query->format.colors.result = defaultResultColor;
868 }
869 if (!setColor(query->format.colors.resultReplacement, u"color-replacement"_s) && hasDefault) {
870 query->format.colors.resultReplacement = ansiBg % u";36m"_sv;
871 }
872
873 if (!optWithError.isEmpty()) {
874 return {Status::Error, u"--"_sv % optWithError % u": invalid color"_sv};
875 }
876 }
877
878 query->fileNames = parser.positionalArguments();
879
880 return {Status::Ok};
881}
882
883void addTextStyleProperties(QJSValue &obj)
884{
886 obj.setProperty(u"dsNormal"_s, TextStyle::Normal);
887 obj.setProperty(u"dsKeyword"_s, TextStyle::Keyword);
888 obj.setProperty(u"dsFunction"_s, TextStyle::Function);
889 obj.setProperty(u"dsVariable"_s, TextStyle::Variable);
890 obj.setProperty(u"dsControlFlow"_s, TextStyle::ControlFlow);
891 obj.setProperty(u"dsOperator"_s, TextStyle::Operator);
892 obj.setProperty(u"dsBuiltIn"_s, TextStyle::BuiltIn);
893 obj.setProperty(u"dsExtension"_s, TextStyle::Extension);
894 obj.setProperty(u"dsPreprocessor"_s, TextStyle::Preprocessor);
895 obj.setProperty(u"dsAttribute"_s, TextStyle::Attribute);
896 obj.setProperty(u"dsChar"_s, TextStyle::Char);
897 obj.setProperty(u"dsSpecialChar"_s, TextStyle::SpecialChar);
898 obj.setProperty(u"dsString"_s, TextStyle::String);
899 obj.setProperty(u"dsVerbatimString"_s, TextStyle::VerbatimString);
900 obj.setProperty(u"dsSpecialString"_s, TextStyle::SpecialString);
901 obj.setProperty(u"dsImport"_s, TextStyle::Import);
902 obj.setProperty(u"dsDataType"_s, TextStyle::DataType);
903 obj.setProperty(u"dsDecVal"_s, TextStyle::DecVal);
904 obj.setProperty(u"dsBaseN"_s, TextStyle::BaseN);
905 obj.setProperty(u"dsFloat"_s, TextStyle::Float);
906 obj.setProperty(u"dsConstant"_s, TextStyle::Constant);
907 obj.setProperty(u"dsComment"_s, TextStyle::Comment);
908 obj.setProperty(u"dsDocumentation"_s, TextStyle::Documentation);
909 obj.setProperty(u"dsAnnotation"_s, TextStyle::Annotation);
910 obj.setProperty(u"dsCommentVar"_s, TextStyle::CommentVar);
911 obj.setProperty(u"dsRegionMarker"_s, TextStyle::RegionMarker);
912 obj.setProperty(u"dsInformation"_s, TextStyle::Information);
913 obj.setProperty(u"dsWarning"_s, TextStyle::Warning);
914 obj.setProperty(u"dsAlert"_s, TextStyle::Alert);
915 obj.setProperty(u"dsOthers"_s, TextStyle::Others);
916 obj.setProperty(u"dsError"_s, TextStyle::Error);
917}
918
919/**
920 * Timestamp in milliseconds.
921 */
922static qsizetype timeNowInMs()
923{
924 auto t = std::chrono::high_resolution_clock::now().time_since_epoch();
925 return std::chrono::duration_cast<std::chrono::milliseconds>(t).count();
926}
927
928QtMessageHandler originalHandler = nullptr;
929/**
930 * Remove messages from kf.sonnet.core when no backend is found.
931 */
932static void filterMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
933{
934 if (originalHandler && context.category != std::string_view("kf.sonnet.core")) {
935 originalHandler(type, context, msg);
936 }
937}
938
939} // anonymous namespace
940
941int main(int ac, char **av)
942{
943 ScriptTesterQuery query;
944 query.xdgDataDirs = qgetenv("XDG_DATA_DIRS");
945
946 qputenv("QT_QPA_PLATFORM", "offscreen"); // equivalent to `-platform offscreen` in cli
947 // Set an unknown folder for XDG_DATA_DIRS so that KateScriptManager::collect()
948 // does not retrieve system scripts.
949 // If the variable is empty, QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)
950 // returns /usr/local/share and /usr/share
951 qputenv("XDG_DATA_DIRS", "/XDG_DATA_DIRS_unknown_folder");
953 originalHandler = qInstallMessageHandler(filterMessageOutput);
954
955 /*
956 * App
957 */
958
959 QApplication app(ac, av);
960 QCoreApplication::setApplicationName(u"katescripttester"_s);
963 QCoreApplication::setApplicationVersion(QStringLiteral(KTEXTEDITOR_VERSION_STRING));
964
965 /*
966 * Cli parser
967 */
968
969 QCommandLineParser parser;
970 initCommandLineParser(app, parser);
971
972 using Status = CommandLineParseResult::Status;
973 CommandLineParseResult parseResult = parseCommandLine(parser, &query);
974 switch (parseResult.statusCode) {
975 case Status::Ok:
976 if (!query.showPreamble && query.fileNames.isEmpty()) {
977 std::fputs("No test file specified.\nUse -h / --help for more details.\n", stderr);
978 return 1;
979 }
980 break;
981 case Status::Error:
982 std::fputs(qPrintable(parseResult.errorString), stderr);
983 std::fputs("\nUse -h / --help for more details.\n", stderr);
984 return 2;
985 case Status::VersionRequested:
986 parser.showVersion();
987 return 0;
988 case Status::HelpRequested:
989 std::fputs(qPrintable(parser.helpText()), stdout);
990 std::fputs(R"(
991Colors:
992 Comma-separated list of values:
993 - color name: black, green, yellow, blue, magenta, cyan, white
994 - bright color name: bright-${color name}
995 - rgb: #fff or #ffffff (use trueColor sequence)
996 - background color: bg=${color name} or bg=bright-${color name} bg=${rgb}
997 - style: bold, dim, italic, underline, reverse, strike, doubly-underline, overlined
998 - ANSI sequence: number sequence with optional ';'
999)",
1000 stdout);
1001 return 0;
1002 }
1003
1004 /*
1005 * Init Preamble
1006 */
1007
1008 // no new line so that the lines indicated by evaluate correspond to the user code
1009 auto jsInjectionStart1 =
1010 u"(function(env, argv){"
1011 u"const TestFramework = this.loadModule(':/ktexteditor/scripttester/testframework.js');"
1012 u"const {REUSE_LAST_INPUT, REUSE_LAST_EXPECTED_OUTPUT} = TestFramework;"
1013 u"const AS_INPUT = TestFramework.EXPECTED_OUTPUT_AS_INPUT;"
1014 u"var {calleeWrapper, config, print, printSep, testCase, sequence, withInput, keys,"
1015 u" indentFiles, test, xtest, eqvTrue, eqvFalse, eqTrue, eqFalse, error, errorMsg,"
1016 u" errorType, hasError, eqv, is, eq, ne, lt, gt, le, ge, cmd, xcmd, type, xtype"
1017 u" } = TestFramework;"
1018 u"var c = TestFramework.sanitizeTag;"
1019 u"var lazyfn = (fn, ...args) => new TestFramework.LazyFunc(fn, ...args);"
1020 u"var fn = lazyfn;"
1021 u"var lazyarg = (arg) => new TestFramework.LazyArg(arg);"
1022 u"var arg = lazyarg;"
1023 u"var loadScript = this.loadScript;"
1024 u"var loadModule = this.loadModule;"
1025 u"var paste = (str) => this.paste(str);"
1026 u"env.editor = TestFramework.editor;" // init editor
1027 u"var document = calleeWrapper('document', env.document);"
1028 u"var editor = calleeWrapper('editor', env.editor);"
1029 u"var view = calleeWrapper('view', env.view);"
1030 u""_sv;
1031 auto debugSetup = query.extendedDebug ? u"debug = testFramework.debug;"_sv : u""_sv;
1032 // clang-format off
1033 auto dualModeSetup = query.dualMode == DualMode::Dual
1034 ? u"const DUAL_MODE = TestFramework.DUAL_MODE;"
1035 u"const ALWAYS_DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"_sv
1036 : query.dualMode == DualMode::NoBlockSelection
1037 ? u"const DUAL_MODE = 0;"
1038 u"const ALWAYS_DUAL_MODE = 0;"_sv
1039 : query.dualMode == DualMode::BlockSelection
1040 ? u"const DUAL_MODE = 1;"
1041 u"const ALWAYS_DUAL_MODE = 1;"_sv
1042 : query.dualMode == DualMode::DualIsAlwaysDual
1043 ? u"const DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"
1044 u"const ALWAYS_DUAL_MODE = TestFramework.ALWAYS_DUAL_MODE;"_sv
1045 // : query.dualMode == DualMode::AlwaysDualIsDual
1046 : u"const DUAL_MODE = TestFramework.DUAL_MODE;"
1047 u"const ALWAYS_DUAL_MODE = TestFramework.DUAL_MODE;"_sv;
1048 // clang-format on
1049 auto jsInjectionStart2 =
1050 u"var kbd = TestFramework.init(this, env, DUAL_MODE);"
1051 u"try { void function(){"_sv;
1052 auto jsInjectionEnd =
1053 u"\n}() }"
1054 u"catch (e) {"
1055 u"if (e !== TestFramework.STOP_CASE_ERROR) {"
1056 u"throw e;"
1057 u"}"
1058 u"}"
1059 u"})\n"
1060 u""_sv;
1061
1062 if (!query.preamble.isEmpty()) {
1063 const auto pattern = u"{CODE}"_sv;
1064 const QStringView preamble = query.preamble;
1065 auto pos = preamble.indexOf(pattern);
1066 if (pos <= -1) {
1067 std::fputs("missing {CODE} with --preamble\n", stderr);
1068 return 2;
1069 }
1070 jsInjectionStart1 = preamble.sliced(0, pos);
1071 jsInjectionEnd = preamble.sliced(pos + pattern.size());
1072 jsInjectionStart2 = QStringView();
1073 dualModeSetup = QStringView();
1074 debugSetup = QStringView();
1075 }
1076
1077 auto makeProgram = [&](QStringView source) -> QString {
1078 return jsInjectionStart1 % debugSetup % dualModeSetup % jsInjectionStart2 % u'\n' % source % jsInjectionEnd;
1079 };
1080
1081 if (query.showPreamble) {
1082 std::fputs(qPrintable(makeProgram(u"{CODE}"_sv)), stdout);
1083 return 0;
1084 }
1085
1086 if (query.restoreXdgDataDirs) {
1087 qputenv("XDG_DATA_DIRS", query.xdgDataDirs);
1088 }
1089
1090 /*
1091 * KTextEditor objects
1092 */
1093
1094 KTextEditor::DocumentPrivate doc(true, false);
1095 KTextEditor::ViewPrivate view(&doc, nullptr);
1096
1097 QJSEngine engine;
1098
1099 KateScriptView viewObj(&engine);
1100 viewObj.setView(&view);
1101
1102 KateScriptDocument docObj(&engine);
1103 docObj.setDocument(&doc);
1104
1105 /*
1106 * ScriptTester object
1107 */
1108
1109 QFile output;
1110 output.open(stderr, QIODevice::WriteOnly);
1111 ScriptTester scriptTester(&output, query.format, query.paths, query.executionConfig, query.diff, defaultPlaceholder, &engine, &doc, &view);
1112
1113 /*
1114 * JS API
1115 */
1116
1117 QJSValue globalObject = engine.globalObject();
1118 QJSValue functions = engine.newQObject(&scriptTester);
1119
1120 globalObject.setProperty(u"read"_s, functions.property(u"read"_s));
1121 globalObject.setProperty(u"require"_s, functions.property(u"require"_s));
1122 globalObject.setProperty(u"debug"_s, functions.property(u"debug"_s));
1123
1124 globalObject.setProperty(u"view"_s, engine.newQObject(&viewObj));
1125 globalObject.setProperty(u"document"_s, engine.newQObject(&docObj));
1126 // editor object is defined later in testframwork.js
1127
1128 addTextStyleProperties(globalObject);
1129
1130 // View and Document expose JS Range objects in the API, which will fail to work
1131 // if Range is not included. range.js includes cursor.js
1132 scriptTester.require(u"range.js"_s);
1133
1134 engine.evaluate(QStringLiteral(
1135 // translation functions (return untranslated text)
1136 "function i18n(text, ...arg) { return text; }\n"
1137 "function i18nc(context, text, ...arg) { return text; }\n"
1138 "function i18np(singular, plural, number, ...arg) { return number > 1 ? plural : singular; }\n"
1139 "function i18ncp(context, singular, plural, number, ...arg) { return number > 1 ? plural : singular; }\n"
1140 // editor object, defined in testframwork.js and built before running a test
1141 "var editor = undefined;"));
1142
1143 /*
1144 * Run function
1145 */
1146
1147 auto jsArgv = engine.newArray(query.argv.size());
1148 for (quint32 i = 0; i < query.argv.size(); ++i) {
1149 jsArgv.setProperty(i, QJSValue(query.argv.constData()[i]));
1150 }
1151
1152 const auto &colors = query.format.colors;
1153
1154 qsizetype delayInMs = 0;
1155 bool resetConfig = false;
1156 auto runProgram = [&](const QString &fileName, const QString &source) {
1157 auto result = engine.evaluate(makeProgram(source), fileName, 0);
1158 if (!result.isError()) {
1159 if (resetConfig) {
1160 scriptTester.resetConfig();
1161 }
1162 resetConfig = true;
1163
1164 for (const auto &variable : std::as_const(query.variables)) {
1165 doc.setVariable(variable.key, variable.value);
1166 }
1167
1168 const auto start = timeNowInMs();
1169 result = result.callWithInstance(functions, {globalObject, jsArgv});
1170 delayInMs += timeNowInMs() - start;
1171 if (!result.isError()) {
1172 return;
1173 }
1174 }
1175
1176 scriptTester.incrementError();
1177 scriptTester.stream() << colors.error << result.toString() << colors.reset << u'\n';
1178 scriptTester.writeException(result, u"| "_sv);
1179 scriptTester.stream().flush();
1180 };
1181
1182 QFile file;
1183 auto runJsFile = [&](const QString &fileName) {
1184 file.setFileName(fileName);
1186 const QString content = ok ? QTextStream(&file).readAll() : QString();
1187 ok = (ok && file.error() == QFileDevice::NoError);
1188 if (!ok) {
1189 scriptTester.incrementError();
1190 scriptTester.stream() << colors.fileName << fileName << colors.reset << ": "_L1 << colors.error << file.errorString() << colors.reset << u'\n';
1191 scriptTester.stream().flush();
1192 }
1193 file.close();
1194 file.unsetError();
1195 if (ok) {
1196 runProgram(fileName, content);
1197 }
1198 };
1199
1200 /*
1201 * Read file and run
1202 */
1203
1204 const auto &fileNames = query.fileNames;
1205 for (const auto &fileName : fileNames) {
1206 if (query.asText) {
1207 runProgram(u"file%1.js"_s.arg(&fileName - fileNames.data() + 1), fileName);
1208 } else if (!QFileInfo(fileName).isDir()) {
1209 runJsFile(fileName);
1210 } else {
1211 QDirIterator it(fileName, {u"*.js"_s}, QDir::Files);
1212 while (it.hasNext() && !scriptTester.hasTooManyErrors()) {
1213 runJsFile(it.next());
1214 }
1215 }
1216
1217 if (scriptTester.hasTooManyErrors()) {
1218 break;
1219 }
1220 }
1221
1222 /*
1223 * Result
1224 */
1225
1226 if (scriptTester.hasTooManyErrors()) {
1227 scriptTester.stream() << colors.error << "Too many error"_L1 << colors.reset << u'\n';
1228 }
1229
1230 scriptTester.writeSummary();
1231 scriptTester.stream() << " Duration: "_L1 << delayInMs << "ms\n"_L1;
1232 scriptTester.stream().flush();
1233
1234 return scriptTester.countError() ? 1 : 0;
1235}
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 Mon Nov 18 2024 12:11:27 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.