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 | |
29 | using namespace Qt::Literals::StringLiterals; |
30 | |
31 | namespace |
32 | { |
33 | |
34 | #if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) |
35 | constexpr QStringView operator""_sv (const char16_t *str, size_t size) noexcept |
36 | { |
37 | return QStringView(str, size); |
38 | } |
39 | #endif |
40 | |
41 | using ScriptTester = KTextEditor::ScriptTester; |
42 | using TextFormat = ScriptTester::DocumentTextFormat; |
43 | using TestFormatOption = ScriptTester::TestFormatOption; |
44 | using PatternType = ScriptTester::PatternType; |
45 | using DebugOption = ScriptTester::DebugOption; |
46 | |
47 | constexpr inline ScriptTester::Format::TextReplacement defaultTextReplacement{ |
48 | .newLine = u'↵', |
49 | .tab1 = u'—', |
50 | .tab2 = u'⇥', |
51 | }; |
52 | constexpr 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 | }; |
61 | constexpr 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 | |
71 | enum class DualMode { |
72 | Dual, |
73 | NoBlockSelection, |
74 | BlockSelection, |
75 | DualIsAlwaysDual, |
76 | AlwaysDualIsDual, |
77 | }; |
78 | |
79 | struct 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 | |
144 | struct 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 | */ |
228 | QString toANSIColor(QStringView str, const QString &defaultColor, bool *ok) |
229 | { |
230 | QString result; |
231 | QVarLengthArray<TrueColor, 8> trueColors; |
232 | |
233 | if (!str.isEmpty()) { |
234 | qsizetype totalLen = 0; |
235 | bool hasDefaultColor = !defaultColor.isEmpty(); |
236 | auto colors = str.split(sep: u',', behavior: 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(s: u"bg=" ); |
258 | auto s = isBg ? color.sliced(pos: 3) : color; |
259 | const bool isBright = s.startsWith(s: u"bright-" ); |
260 | s = isBright ? s.sliced(pos: 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(color: 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(asize: 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(t&: 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 | |
357 | void initCommandLineParser(QCoreApplication &app, QCommandLineParser &parser) |
358 | { |
359 | auto tr = [&app](char const *s) { |
360 | return app.translate(context: "KateScriptTester" , key: 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(name: tr("file.js" ), description: tr("Test files to run. If file.js represents a folder, this is equivalent to `path/*.js`." ), syntax: tr("file.js..." )); |
371 | |
372 | parser.addOptions(options: { |
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 | |
537 | struct CommandLineParseResult { |
538 | enum class Status { |
539 | Ok, |
540 | Error, |
541 | VersionRequested, |
542 | HelpRequested |
543 | }; |
544 | Status statusCode = Status::Ok; |
545 | QString errorString = {}; |
546 | }; |
547 | |
548 | CommandLineParseResult 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(arguments: QCoreApplication::arguments())) |
556 | return {.statusCode: Status::Error, .errorString: parser.errorText()}; |
557 | |
558 | if (parser.isSet(name: u"v"_s )) |
559 | return {.statusCode: Status::VersionRequested}; |
560 | |
561 | if (parser.isSet(name: u"h"_s )) |
562 | return {.statusCode: Status::HelpRequested}; |
563 | |
564 | query->asText = parser.isSet(name: u"t"_s ); |
565 | |
566 | if (parser.isSet(name: u"q"_s )) { |
567 | query->executionConfig.maxError = 1; |
568 | } |
569 | if (parser.isSet(name: u"e"_s )) { |
570 | bool ok = true; |
571 | query->executionConfig.maxError = parser.value(name: u"e"_s ).toInt(ok: &ok); |
572 | if (!ok) { |
573 | return {.statusCode: Status::Error, .errorString: u"--max-error: invalid number"_s }; |
574 | } |
575 | } |
576 | query->executionConfig.xCheckAsFailure = parser.isSet(name: u"E"_s ); |
577 | |
578 | if (parser.isSet(name: u"s"_s )) { |
579 | auto addPath = [](QStringList &l, QString path) { |
580 | if (QFile::exists(fileName: path)) { |
581 | l.append(t: path); |
582 | } |
583 | }; |
584 | const auto paths = parser.values(name: 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(name: opt)) { |
595 | const auto paths = parser.values(name: opt); |
596 | for (const auto &path : paths) { |
597 | l.append(t: 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(name: u"I"_s ); |
607 | |
608 | if (parser.isSet(name: u"d"_s )) { |
609 | const auto value = parser.value(name: 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(flag: DebugOption::WriteLocation, on: false); |
622 | } else if (value == u"no-function"_sv ) { |
623 | query->format.debugOptions.setFlag(flag: DebugOption::WriteFunction, on: false); |
624 | } else if (value == u"no-stacktrace"_sv ) { |
625 | query->format.debugOptions.setFlag(flag: DebugOption::WriteStackTrace, on: false); |
626 | } else if (value == u"no-flush"_sv ) { |
627 | query->format.debugOptions.setFlag(flag: DebugOption::ForceFlush, on: 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 {.statusCode: Status::Error, .errorString: u"--debug: invalid value"_s }; |
638 | } |
639 | } |
640 | |
641 | if (parser.isSet(name: u"H"_s )) { |
642 | query->format.testFormatOptions |= TestFormatOption::HiddenTestName; |
643 | } |
644 | if (parser.isSet(name: u"p"_s )) { |
645 | query->format.testFormatOptions |= TestFormatOption::AlwaysWriteLocation; |
646 | } |
647 | if (parser.isSet(name: u"V"_s )) { |
648 | query->format.testFormatOptions |= TestFormatOption::AlwaysWriteInputOutput; |
649 | } |
650 | |
651 | auto setFormat = [&parser](TextFormat &textFormat, const QString &opt) { |
652 | if (parser.isSet(name: opt)) { |
653 | const auto value = parser.value(name: 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 {.statusCode: Status::Error, .errorString: u"--format: invalid value"_s }; |
672 | } |
673 | if (!setFormat(query->format.documentTextFormatWithBlockSelection, u"F"_s )) { |
674 | return {.statusCode: Status::Error, .errorString: u"--block-format: invalid value"_s }; |
675 | } |
676 | |
677 | auto setPattern = [&parser, &query](QString opt, PatternType patternType) { |
678 | if (parser.isSet(name: opt)) { |
679 | query->executionConfig.pattern.setPatternOptions(QRegularExpression::DontCaptureOption | QRegularExpression::UseUnicodePropertiesOption); |
680 | query->executionConfig.pattern.setPattern(parser.value(name: 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 {.statusCode: Status::Error, .errorString: u"-k: "_sv + query->executionConfig.pattern.errorString()}; |
691 | } |
692 | if (!setPattern(u"K"_s , PatternType::Exclude)) { |
693 | return {.statusCode: Status::Error, .errorString: u"-K: "_sv + query->executionConfig.pattern.errorString()}; |
694 | } |
695 | |
696 | if (parser.isSet(name: u"T"_s )) { |
697 | const auto tab = parser.value(name: 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(name: u"N"_s )) { |
712 | const auto nl = parser.value(name: u"N"_s ); |
713 | query->format.textReplacement.newLine = getChar(nl, 0, query->format.textReplacement.newLine); |
714 | } |
715 | |
716 | if (parser.isSet(name: u"P"_s )) { |
717 | const auto symbols = parser.value(name: 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(name: u"B"_s )) { |
729 | query->dualMode = DualMode::NoBlockSelection; |
730 | } |
731 | |
732 | if (parser.isSet(name: u"b"_s )) { |
733 | const auto mode = parser.value(name: 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 {.statusCode: Status::Error, .errorString: u"--dual: invalid value"_s }; |
746 | } |
747 | query->dualMode = DualMode::NoBlockSelection; |
748 | } |
749 | |
750 | query->argv = parser.values(name: u"arg"_s ); |
751 | |
752 | if (parser.isSet(name: u"preamble"_s )) { |
753 | query->preamble = parser.value(name: u"preamble"_s ); |
754 | } |
755 | |
756 | query->showPreamble = parser.isSet(name: u"print-preamble"_s ); |
757 | |
758 | if (parser.isSet(name: u"X"_s )) { |
759 | query->xdgDataDirs = parser.value(name: u"X"_s ).toUtf8(); |
760 | query->restoreXdgDataDirs = true; |
761 | } else { |
762 | query->restoreXdgDataDirs = parser.isSet(name: u"x"_s ); |
763 | } |
764 | |
765 | if (parser.isSet(name: u"S"_s )) { |
766 | const auto variables = parser.values(name: u"S"_s ); |
767 | query->variables.resize(size: variables.size()); |
768 | auto it = query->variables.begin(); |
769 | for (const auto &kv : variables) { |
770 | auto pos = QStringView(kv).indexOf(c: u'='); |
771 | if (pos >= 0) { |
772 | it->key = kv.sliced(pos: 0, n: pos); |
773 | it->value = kv.sliced(pos: pos + 1); |
774 | } else { |
775 | it->key = kv; |
776 | } |
777 | ++it; |
778 | } |
779 | } |
780 | |
781 | query->diff.path = parser.isSet(name: u"D"_s ) ? parser.value(name: u"D"_s ) : u"diff"_s ; |
782 | |
783 | const bool noColor = parser.isSet(name: u"no-color"_s ); |
784 | |
785 | if (parser.isSet(name: u"A"_s )) { |
786 | query->diff.args = parser.values(name: u"A"_s ); |
787 | } else { |
788 | query->diff.args.push_back(t: u"-u"_s ); |
789 | if (!noColor) { |
790 | query->diff.args.push_back(t: 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(name: opt)) { |
821 | bool ok = true; |
822 | color = toANSIColor(str: parser.value(name: opt), defaultColor: defaultResultColor, ok: &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 {.statusCode: Status::Error, .errorString: u"--"_sv % optWithError % u": invalid color"_sv }; |
877 | } |
878 | } |
879 | |
880 | query->fileNames = parser.positionalArguments(); |
881 | |
882 | return {.statusCode: Status::Ok}; |
883 | } |
884 | |
885 | void addTextStyleProperties(QJSValue &obj) |
886 | { |
887 | using TextStyle = KSyntaxHighlighting::Theme::TextStyle; |
888 | obj.setProperty(name: u"dsNormal"_s , value: TextStyle::Normal); |
889 | obj.setProperty(name: u"dsKeyword"_s , value: TextStyle::Keyword); |
890 | obj.setProperty(name: u"dsFunction"_s , value: TextStyle::Function); |
891 | obj.setProperty(name: u"dsVariable"_s , value: TextStyle::Variable); |
892 | obj.setProperty(name: u"dsControlFlow"_s , value: TextStyle::ControlFlow); |
893 | obj.setProperty(name: u"dsOperator"_s , value: TextStyle::Operator); |
894 | obj.setProperty(name: u"dsBuiltIn"_s , value: TextStyle::BuiltIn); |
895 | obj.setProperty(name: u"dsExtension"_s , value: TextStyle::Extension); |
896 | obj.setProperty(name: u"dsPreprocessor"_s , value: TextStyle::Preprocessor); |
897 | obj.setProperty(name: u"dsAttribute"_s , value: TextStyle::Attribute); |
898 | obj.setProperty(name: u"dsChar"_s , value: TextStyle::Char); |
899 | obj.setProperty(name: u"dsSpecialChar"_s , value: TextStyle::SpecialChar); |
900 | obj.setProperty(name: u"dsString"_s , value: TextStyle::String); |
901 | obj.setProperty(name: u"dsVerbatimString"_s , value: TextStyle::VerbatimString); |
902 | obj.setProperty(name: u"dsSpecialString"_s , value: TextStyle::SpecialString); |
903 | obj.setProperty(name: u"dsImport"_s , value: TextStyle::Import); |
904 | obj.setProperty(name: u"dsDataType"_s , value: TextStyle::DataType); |
905 | obj.setProperty(name: u"dsDecVal"_s , value: TextStyle::DecVal); |
906 | obj.setProperty(name: u"dsBaseN"_s , value: TextStyle::BaseN); |
907 | obj.setProperty(name: u"dsFloat"_s , value: TextStyle::Float); |
908 | obj.setProperty(name: u"dsConstant"_s , value: TextStyle::Constant); |
909 | obj.setProperty(name: u"dsComment"_s , value: TextStyle::Comment); |
910 | obj.setProperty(name: u"dsDocumentation"_s , value: TextStyle::Documentation); |
911 | obj.setProperty(name: u"dsAnnotation"_s , value: TextStyle::Annotation); |
912 | obj.setProperty(name: u"dsCommentVar"_s , value: TextStyle::CommentVar); |
913 | obj.setProperty(name: u"dsRegionMarker"_s , value: TextStyle::RegionMarker); |
914 | obj.setProperty(name: u"dsInformation"_s , value: TextStyle::Information); |
915 | obj.setProperty(name: u"dsWarning"_s , value: TextStyle::Warning); |
916 | obj.setProperty(name: u"dsAlert"_s , value: TextStyle::Alert); |
917 | obj.setProperty(name: u"dsOthers"_s , value: TextStyle::Others); |
918 | obj.setProperty(name: u"dsError"_s , value: TextStyle::Error); |
919 | } |
920 | |
921 | /** |
922 | * Timestamp in milliseconds. |
923 | */ |
924 | static qsizetype timeNowInMs() |
925 | { |
926 | auto t = std::chrono::high_resolution_clock::now().time_since_epoch(); |
927 | return std::chrono::duration_cast<std::chrono::milliseconds>(d: t).count(); |
928 | } |
929 | |
930 | QtMessageHandler originalHandler = nullptr; |
931 | /** |
932 | * Remove messages from kf.sonnet.core when no backend is found. |
933 | */ |
934 | static 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 | |
943 | int main(int ac, char **av) |
944 | { |
945 | ScriptTesterQuery query; |
946 | query.xdgDataDirs = qgetenv(varName: "XDG_DATA_DIRS" ); |
947 | |
948 | qputenv(varName: "QT_QPA_PLATFORM" , value: "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(varName: "XDG_DATA_DIRS" , value: "/XDG_DATA_DIRS_unknown_folder" ); |
954 | QStandardPaths::setTestModeEnabled(true); |
955 | originalHandler = qInstallMessageHandler(filterMessageOutput); |
956 | |
957 | /* |
958 | * App |
959 | */ |
960 | |
961 | QApplication app(ac, av); |
962 | QCoreApplication::setApplicationName(u"katescripttester"_s ); |
963 | QCoreApplication::setOrganizationDomain(u"kde.org"_s ); |
964 | QCoreApplication::setOrganizationName(u"KDE"_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: &query); |
976 | switch (parseResult.statusCode) { |
977 | case Status::Ok: |
978 | if (!query.showPreamble && query.fileNames.isEmpty()) { |
979 | std::fputs(s: "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(s: "\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(s: R"( |
993 | Colors: |
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(s: pattern); |
1068 | if (pos <= -1) { |
1069 | std::fputs(s: "missing {CODE} with --preamble\n" , stderr); |
1070 | return 2; |
1071 | } |
1072 | jsInjectionStart1 = preamble.sliced(pos: 0, n: pos); |
1073 | jsInjectionEnd = preamble.sliced(pos: 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(varName: "XDG_DATA_DIRS" , value: 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, ioFlags: 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(object: &scriptTester); |
1121 | |
1122 | globalObject.setProperty(name: u"read"_s , value: functions.property(name: u"read"_s )); |
1123 | globalObject.setProperty(name: u"require"_s , value: functions.property(name: u"require"_s )); |
1124 | globalObject.setProperty(name: u"debug"_s , value: functions.property(name: u"debug"_s )); |
1125 | |
1126 | globalObject.setProperty(name: u"view"_s , value: engine.newQObject(object: &viewObj)); |
1127 | globalObject.setProperty(name: u"document"_s , value: engine.newQObject(object: &docObj)); |
1128 | // editor object is defined later in testframwork.js |
1129 | |
1130 | addTextStyleProperties(obj&: 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(file: 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(length: query.argv.size()); |
1150 | for (quint32 i = 0; i < query.argv.size(); ++i) { |
1151 | jsArgv.setProperty(arrayIndex: i, value: 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(program: makeProgram(source), fileName, lineNumber: 0); |
1160 | if (!result.isError()) { |
1161 | if (resetConfig) { |
1162 | scriptTester.resetConfig(); |
1163 | } |
1164 | resetConfig = true; |
1165 | |
1166 | for (const auto &variable : std::as_const(t&: query.variables)) { |
1167 | doc.setVariable(name: variable.key, value: variable.value); |
1168 | } |
1169 | |
1170 | const auto start = timeNowInMs(); |
1171 | result = result.callWithInstance(instance: functions, args: {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(exception: result, prefix: u"| "_sv ); |
1181 | scriptTester.stream().flush(); |
1182 | }; |
1183 | |
1184 | QFile file; |
1185 | auto runJsFile = [&](const QString &fileName) { |
1186 | file.setFileName(fileName); |
1187 | bool ok = file.open(flags: QIODevice::ReadOnly | QIODevice::Text); |
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(a: &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 | } |
1238 | |