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