1 | /* |
2 | SPDX-FileCopyrightText: 2024 Jonathan Poelen <jonathan.poelen@gmail.com> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #ifndef KTEXTEDITOR_SCRIPT_TESTER_HELPERS_H |
8 | #define KTEXTEDITOR_SCRIPT_TESTER_HELPERS_H |
9 | |
10 | #include <ktexteditor/cursor.h> |
11 | #include <ktexteditor/range.h> |
12 | |
13 | #include "kateview.h" |
14 | |
15 | #include <QFlags> |
16 | #include <QJSValue> |
17 | #include <QMap> |
18 | #include <QObject> |
19 | #include <QRegularExpression> |
20 | #include <QStringList> |
21 | #include <QStringView> |
22 | #include <QTextStream> |
23 | |
24 | #include <vector> |
25 | |
26 | class QJSEngine; |
27 | |
28 | namespace KTextEditor |
29 | { |
30 | |
31 | class DocumentPrivate; |
32 | |
33 | /** |
34 | * Enables unit tests to be run on js scripts (commands, indentations, libraries). |
35 | * |
36 | * Unit tests are written in javascript via a module called TestFramework. |
37 | * |
38 | * This class configures a document and a view through strings representing |
39 | * the document text and the position of cursors and selections via special |
40 | * characters called placeholders. |
41 | * |
42 | * For example, using \c setInput() with `"a[bc]|d"` will put the text "abcd" |
43 | * in the document, a selection from the second to the third character and |
44 | * place the cursor on the latter. |
45 | */ |
46 | class KTEXTEDITOR_NO_EXPORT ScriptTester : public QObject |
47 | { |
48 | Q_OBJECT |
49 | |
50 | public: |
51 | /** |
52 | * Controls the behavior of the debug function. |
53 | */ |
54 | enum class DebugOption : unsigned char { |
55 | None, |
56 | /// Add location before log (file name and line number). |
57 | WriteLocation = 1 << 0, |
58 | /// Add function name before log . |
59 | WriteFunction = 1 << 1, |
60 | /// Add stacktrace after log. |
61 | WriteStackTrace = 1 << 2, |
62 | /// Forces writing every time \c debug is called. |
63 | /// Otherwise, the log is only visible if the test fails. |
64 | ForceFlush = 1 << 3, |
65 | }; |
66 | Q_DECLARE_FLAGS(DebugOptions, DebugOption) |
67 | |
68 | enum class TestFormatOption : unsigned char { |
69 | None, |
70 | AlwaysWriteInputOutput = 1 << 0, |
71 | AlwaysWriteLocation = 1 << 1, |
72 | HiddenTestName = 1 << 2, |
73 | }; |
74 | Q_DECLARE_FLAGS(TestFormatOptions, TestFormatOption) |
75 | |
76 | /** |
77 | * Format used for input, output and expectedOutput of command display. |
78 | */ |
79 | enum class DocumentTextFormat : unsigned char { |
80 | /// No transformation. |
81 | Raw, |
82 | /// Formats to a valid javascript String. |
83 | EscapeForDoubleQuote, |
84 | /// Replace new line with \c \n and tab with \c \t. |
85 | ReplaceNewLineAndTabWithLiteral, |
86 | /// Replace new line and tab with a placeholder (see \ref Format::TextReplacement) |
87 | ReplaceNewLineAndTabWithPlaceholder, |
88 | /// Replace tab with a placeholder (see \ref Format::TextReplacement) |
89 | ReplaceTabWithPlaceholder, |
90 | }; |
91 | |
92 | enum class PatternType : unsigned char { |
93 | /// No filter, all tests will be executed. |
94 | Inactive, |
95 | /// Only tests whose pattern matches will be executed. |
96 | Include, |
97 | /// Tests whose pattern matches will be ignored. |
98 | Exclude, |
99 | }; |
100 | |
101 | /** |
102 | * Placeholders to represent cursor, selection and virtual text in a string. |
103 | * A placeholder is used if it is activated (\c enabled) |
104 | * and the character value is not 0. |
105 | */ |
106 | struct Placeholders { |
107 | QChar cursor; |
108 | QChar selectionStart; |
109 | QChar selectionEnd; |
110 | QChar secondaryCursor; |
111 | QChar secondarySelectionStart; |
112 | QChar secondarySelectionEnd; |
113 | // Text to insert "spaces" for block selection outside lines. |
114 | // However, the new line must come after. |
115 | QChar virtualText; |
116 | |
117 | /** |
118 | * Check that a placeholder is not 0. |
119 | */ |
120 | /// @{ |
121 | bool hasCursor() const |
122 | { |
123 | return cursor.unicode(); |
124 | } |
125 | |
126 | bool hasSelection() const |
127 | { |
128 | return selectionStart.unicode() && selectionEnd.unicode(); |
129 | } |
130 | |
131 | bool hasSecondaryCursor() const |
132 | { |
133 | return secondaryCursor.unicode(); |
134 | } |
135 | |
136 | bool hasSecondarySelection() const |
137 | { |
138 | return secondarySelectionStart.unicode() && secondarySelectionEnd.unicode(); |
139 | } |
140 | |
141 | bool hasVirtualText() const |
142 | { |
143 | return virtualText.unicode(); |
144 | } |
145 | /// @} |
146 | }; |
147 | |
148 | /** |
149 | * ANSI character sequence insert into output. |
150 | */ |
151 | struct Colors { |
152 | QString reset; |
153 | /// Number of successful tests. |
154 | QString success; |
155 | /// Error in tests or exception. |
156 | QString error; |
157 | /// "^~~" sequence under error position. |
158 | QString carret = error; |
159 | /// "DEBUG:" prefix with \c debug(). |
160 | QString debugMarker = error; |
161 | /// Debug message with \c debug(). |
162 | QString debugMsg = error; |
163 | /// Name of the test passed to testCase and co. |
164 | QString testName; |
165 | /// Program paramater in \c cmd() / \c test(). Function name in stacktrace. |
166 | QString program; |
167 | QString fileName; |
168 | QString lineNumber; |
169 | /// [blockSelection=...] displayed as information in a check. |
170 | QString blockSelectionInfo; |
171 | /// "input", "output", "result" label when it is displayed as information and not as an error. |
172 | QString labelInfo; |
173 | /// Cursor placeholder. |
174 | QString cursor; |
175 | /// Selection placeholder. |
176 | QString selection; |
177 | /// Secondary cursor placeholder. |
178 | QString secondaryCursor; |
179 | /// Secondary selection placeholder. |
180 | QString secondarySelection; |
181 | /// Block selection placeholder. |
182 | QString blockSelection; |
183 | /// Text inside a selection. Style is added. |
184 | QString inSelection; |
185 | /// Virtual text placeholder. |
186 | QString virtualText; |
187 | /// Text representing the inputs and outputs of a test. |
188 | QString result; |
189 | /// Text replaced by \ref DocumentTextFormat / \ref TextReplacement. |
190 | QString resultReplacement; |
191 | }; |
192 | |
193 | /** |
194 | * Groups the options that control the display of tests. |
195 | */ |
196 | struct Format { |
197 | /** |
198 | * Placeholder for \ref DocumentTextFormat. |
199 | */ |
200 | struct TextReplacement { |
201 | /// Character inserted at end of line. |
202 | /// Useful for distinguishing end-of-line spaces. |
203 | QChar newLine; |
204 | /// Character used to replace a tab (repeated \c tabWidth times). |
205 | QChar tab1; |
206 | /// Character used to replace last char in tab. |
207 | /// This distinguishes 2 successive tabs: |
208 | /// For \p tab1 = '-' and \p tab2 = '>' with \c tabWidth = 4 |
209 | /// then \c '\t\t\t' is replaced by \c '--->--->--->'. |
210 | QChar tab2; |
211 | }; |
212 | |
213 | DebugOptions debugOptions; |
214 | TestFormatOptions testFormatOptions; |
215 | DocumentTextFormat documentTextFormat; |
216 | DocumentTextFormat documentTextFormatWithBlockSelection; |
217 | TextReplacement textReplacement; |
218 | /** |
219 | * Placeholder used for display when \c cursor is identical to |
220 | * \c selectionStart, \c selectionEnd, represents a new line or 0 |
221 | * (idem for secondary cursor). |
222 | */ |
223 | Placeholders fallbackPlaceholders; |
224 | Colors colors; |
225 | }; |
226 | |
227 | /** |
228 | * Folder path for javascript scripts and data test. |
229 | */ |
230 | struct Paths { |
231 | /// Paths for \ref loadScript(). |
232 | QStringList scripts; |
233 | /// Paths for \ref require() (KTextEditor JS API). |
234 | QStringList libraries; |
235 | /// Paths for \ref read() (KTextEditor JS API). |
236 | QStringList files; |
237 | /// Paths for \ref loadModule(). |
238 | QStringList modules; |
239 | /// Base path for \ref testIndentFiles(). |
240 | QString indentBaseDir; |
241 | }; |
242 | |
243 | struct TestExecutionConfig { |
244 | /** |
245 | * maximum number of tests that can fail before the framework |
246 | * returns a StopCaseError and execution stops. |
247 | * Negative value or 0 means infinity. |
248 | */ |
249 | int maxError = 0; |
250 | |
251 | /** |
252 | * When true, xcmd() and xtest() functions will always return a failure. |
253 | */ |
254 | bool xCheckAsFailure = false; |
255 | |
256 | /** |
257 | * A pattern include or exclude test. |
258 | */ |
259 | QRegularExpression pattern{}; |
260 | PatternType patternType = PatternType::Inactive; |
261 | }; |
262 | |
263 | /** |
264 | * Diff command for \c testIndentFiles(). |
265 | */ |
266 | struct DiffCommand { |
267 | QString path; |
268 | QStringList args; |
269 | }; |
270 | |
271 | explicit ScriptTester(QIODevice *output, |
272 | const Format &format, |
273 | const Paths &paths, |
274 | const TestExecutionConfig &executionConfig, |
275 | const DiffCommand &diffCmd, |
276 | Placeholders placeholders, |
277 | QJSEngine *engine, |
278 | DocumentPrivate *doc, |
279 | ViewPrivate *view, |
280 | QObject *parent = nullptr); |
281 | |
282 | ScriptTester(const ScriptTester &) = delete; |
283 | ScriptTester &operator=(const ScriptTester &) = delete; |
284 | |
285 | QTextStream &stream() |
286 | { |
287 | return m_stream; |
288 | } |
289 | |
290 | // KTextEditor API |
291 | //@{ |
292 | /// See \ref Paths. |
293 | Q_INVOKABLE QString read(const QString &file); |
294 | /// See \ref Paths. |
295 | Q_INVOKABLE void require(const QString &file); |
296 | /// See \ref DebugOption. |
297 | Q_INVOKABLE void debug(const QString &msg); |
298 | //@} |
299 | |
300 | // Keyboard |
301 | //@{ |
302 | Q_INVOKABLE void type(const QString &str); |
303 | Q_INVOKABLE void enter(); |
304 | //@} |
305 | |
306 | // Utility function |
307 | //@{ |
308 | Q_INVOKABLE void paste(const QString &str); |
309 | //@} |
310 | |
311 | /** |
312 | * Test the indentation of all files in the \p dataDir folder. |
313 | * This folder must contain subfolders with \c origin and \c expected files. |
314 | * If the result is different, a file named \c actual will be written and |
315 | * the difference will be displayed. |
316 | * @param name name of test |
317 | * @param dataDir path of directory |
318 | * @param nthStack call stack line where to find test file name and line number |
319 | * @return true when the indentation matches the expected file. |
320 | */ |
321 | Q_INVOKABLE bool testIndentFiles(const QString &name, const QString &dataDir, int nthStack, bool exitOnError); |
322 | |
323 | /** |
324 | * Load a javascript module. |
325 | * @param fileName file name or path of js module. If the path starts with |
326 | * "./" or "../", then the search is relative to the file calling the |
327 | * function. Other relative paths are relative to \ref JSPath.modules. |
328 | */ |
329 | Q_INVOKABLE QJSValue loadModule(const QString &fileName); |
330 | |
331 | /** |
332 | * Load a javascript script command. |
333 | * @param fileName file name or path of js script. If the path starts with |
334 | * "./" or "../", then the search is relative to the file calling the |
335 | * function. Other relative paths are relative to \ref JSPath.scripts. |
336 | */ |
337 | Q_INVOKABLE void loadScript(const QString &fileName); |
338 | |
339 | /** |
340 | * Like \c debug, but not buffered. |
341 | */ |
342 | Q_INVOKABLE void print(const QString &msg); |
343 | |
344 | /** |
345 | * Format and write an exception. |
346 | */ |
347 | void writeException(const QJSValue &exception, QStringView prefix); |
348 | |
349 | /** |
350 | * Displays test information such as the number of successes or failures. |
351 | */ |
352 | void writeSummary(); |
353 | |
354 | /** |
355 | * Reset all counters. |
356 | */ |
357 | void resetCounters(); |
358 | |
359 | /** |
360 | * @param name name of test |
361 | * @param nthStack call stack line where to find test file name and line number |
362 | * @return true if tests can start, otherwise false (when filtered) |
363 | */ |
364 | Q_INVOKABLE bool startTestCase(const QString &name, int nthStack); |
365 | |
366 | /** |
367 | * Config for \c Placeholders \ref DocumentPrivate and \ref ViewPrivate. |
368 | * An empty string disables a placeholder. |
369 | * A string in a fallback placeholder (cursor2, selection2, etc) resets it |
370 | * to the default. |
371 | */ |
372 | Q_INVOKABLE void setConfig(const QJSValue &config); |
373 | |
374 | /** |
375 | * Reset the configuraion to the original state. |
376 | */ |
377 | Q_INVOKABLE void resetConfig(); |
378 | |
379 | /** |
380 | * Saves the last state of the configuration to restore it later. |
381 | */ |
382 | Q_INVOKABLE void pushConfig(); |
383 | |
384 | /** |
385 | * Restores the last saved configuration. |
386 | */ |
387 | Q_INVOKABLE void popConfig(); |
388 | |
389 | /** |
390 | * Evaluates \p program and returns the result of the evaluation. |
391 | * @param program |
392 | * @return result of program |
393 | */ |
394 | Q_INVOKABLE QJSValue evaluate(const QString &program); |
395 | |
396 | /** |
397 | * Set a input document text, cursor and selection positions according to |
398 | * its placeholders. |
399 | * |
400 | * - If no cursor is specified, then it will be placed at the end of the |
401 | * selection if present, or at the end of the document. |
402 | * |
403 | * - If several secondary cursors are specified, but none primary, then the |
404 | * first secondary cursor will be a primary cursor. |
405 | * The same applies to selection. |
406 | * |
407 | * - The start of a selection must be before the end of a selection. |
408 | * If a cursor is to be located on the 'start' part of the selection, |
409 | * then either it must be explicitly indicated, or the 'cursor' |
410 | * placeholder must have the same value as 'selectionStart'. |
411 | * |
412 | * - Several placeholders can have the same value, but they must not be |
413 | * identical to the 'virtualText' placeholder. |
414 | * |
415 | * - In the case of virtual text, the cursor is automatically placed at the |
416 | * end of the line if block selection is disabled. No text character |
417 | * other than the line feed may be present after a virtual text. |
418 | * Placeholders are of course permitted. |
419 | * |
420 | * @param input input document text with placeholders. |
421 | * @param blockSelection indicates whether the display should use block selection |
422 | */ |
423 | Q_INVOKABLE void setInput(const QString &input, bool blockSelection); |
424 | |
425 | /** |
426 | * Move the expected output previously defined as input text value. |
427 | * @param blockSelection indicates whether the display should use block selection |
428 | */ |
429 | Q_INVOKABLE void moveExpectedOutputToInput(bool blockSelection); |
430 | |
431 | /** |
432 | * Reset input document text to previously defined input value. |
433 | * @param blockSelection indicates whether the display should use block selection |
434 | */ |
435 | Q_INVOKABLE void reuseInput(bool blockSelection); |
436 | |
437 | /** |
438 | * As \c reuseInput, but checks that block selection mode is possible. |
439 | * @return true if block selection mode is compatible. Otherwise false. |
440 | */ |
441 | Q_INVOKABLE bool reuseInputWithBlockSelection(); |
442 | |
443 | /** |
444 | * Same as \c setInput(), but for expected output document text. |
445 | * @param expected output document text with placeholders. |
446 | * @param blockSelection indicates whether the display should use block selection |
447 | */ |
448 | Q_INVOKABLE void setExpectedOutput(const QString &expected, bool blockSelection); |
449 | |
450 | /** |
451 | * Reset expected output document text to previously defined expected output value. |
452 | * @param blockSelection indicates whether the display should use block selection |
453 | */ |
454 | Q_INVOKABLE void reuseExpectedOutput(bool blockSelection); |
455 | |
456 | /** |
457 | * Set expected output with the same value as input. |
458 | * @param blockSelection indicates whether the display should use block selection |
459 | */ |
460 | Q_INVOKABLE void copyInputToExpectedOutput(bool blockSelection); |
461 | |
462 | /** |
463 | * Check that the output corresponds to the expected output. |
464 | * @return bool \c true if the values are identical, otherwise \c false. |
465 | */ |
466 | Q_INVOKABLE bool checkOutput(); |
467 | |
468 | /** |
469 | * Increment the success counter or the failure counter. |
470 | * @param isSuccessNotAFailure \c true for the success counter, \c false for the failure counter |
471 | * @param xcheck Boolean: true for an expected failure |
472 | * @return \p isSuccessNotAFailure |
473 | */ |
474 | Q_INVOKABLE bool incrementCounter(bool isSuccessNotAFailure, bool xcheck); |
475 | |
476 | /** |
477 | * Increment the error counter. |
478 | */ |
479 | Q_INVOKABLE void incrementError(); |
480 | |
481 | /** |
482 | * Increment the break test case counter. |
483 | * This counter corresponds to the test cases that are stopped following a failure. |
484 | */ |
485 | Q_INVOKABLE void incrementBreakOnError(); |
486 | |
487 | /** |
488 | * Return error and failure count. |
489 | */ |
490 | Q_INVOKABLE int countError() const; |
491 | |
492 | /** |
493 | * Check if the number of errors is too high. |
494 | */ |
495 | Q_INVOKABLE bool hasTooManyErrors() const; |
496 | |
497 | /** |
498 | * Start a check. |
499 | * @return flags with 0x1 when \c writeTestResult must always be called |
500 | * and 0x2 when \c writeTestExpression must always called |
501 | */ |
502 | Q_INVOKABLE int startTest(); |
503 | |
504 | /** |
505 | * Complete a test. |
506 | */ |
507 | Q_INVOKABLE void endTest(bool ok, bool showBlockSelection = false); |
508 | |
509 | /** |
510 | * Write a test. |
511 | * @param name name of test |
512 | * @param type test type ("cmd" or "test") |
513 | * @param nthStack call stack line where to find test file name and line number |
514 | * @param program program used for test |
515 | */ |
516 | Q_INVOKABLE void writeTestExpression(const QString &name, const QString &type, int nthStack, const QString &program); |
517 | |
518 | /** |
519 | * Write a test aborted. |
520 | * @param name name of test |
521 | * @param nthStack call stack line where to find test file name and line number |
522 | */ |
523 | Q_INVOKABLE void writeDualModeAborted(const QString &name, int nthStack); |
524 | |
525 | /** |
526 | * Write a test result. |
527 | * @param name name of test |
528 | * @param type test type ("cmd" or "test") |
529 | * @param nthStack call stack line where to find test file name and line number |
530 | * @param program program used for test |
531 | * @param msg user message |
532 | * @param exception program exception |
533 | * @param result program result in displayable format |
534 | * @param expectedResult expected program result in a displayable format |
535 | * @param options display options |
536 | */ |
537 | Q_INVOKABLE void writeTestResult(const QString &name, |
538 | const QString &type, |
539 | int nthStack, |
540 | const QString &program, |
541 | const QString &msg, |
542 | const QJSValue &exception, |
543 | const QString &result, |
544 | const QString &expectedResult, |
545 | int options); |
546 | |
547 | private: |
548 | struct Replacements; |
549 | struct TextItem; |
550 | |
551 | struct DocumentText { |
552 | std::vector<TextItem> items; |
553 | QString text; |
554 | Cursor cursor; |
555 | Range selection; |
556 | // secondary cursor with selection |
557 | QList<ViewPrivate::PlainSecondaryCursor> secondaryCursorsWithSelection; |
558 | std::vector<ViewPrivate::PlainSecondaryCursor> secondaryCursors; |
559 | int totalLine = 0; |
560 | // used with checkMultiCursorCompatibility(), ignored for m_output |
561 | //@{ |
562 | int totalCursor = 0; |
563 | int totalSelection = 0; |
564 | //@} |
565 | bool hasFormattingItems = false; |
566 | bool hasBlockSelectionItems = false; |
567 | bool blockSelection = false; |
568 | |
569 | DocumentText(); |
570 | ~DocumentText(); |
571 | DocumentText(DocumentText &&) = default; |
572 | DocumentText &operator=(DocumentText &&) = default; |
573 | DocumentText &operator=(DocumentText const &) = default; |
574 | |
575 | QString setText(QStringView input, const Placeholders &placeholders); |
576 | |
577 | std::size_t addItems(QStringView str, int kind, QChar c); |
578 | std::size_t addSelectionItems(QStringView str, int kind, QChar start, QChar end); |
579 | |
580 | void computeBlockSelectionItems(); |
581 | |
582 | void insertFormattingItems(DocumentTextFormat format); |
583 | |
584 | void sortItems(); |
585 | }; |
586 | |
587 | /** |
588 | * Init config of m_view and m_doc. |
589 | */ |
590 | void initDocConfig(); |
591 | void syncIndenter(); |
592 | |
593 | /** |
594 | * Init m_view and m_doc. |
595 | */ |
596 | void initInputDoc(); |
597 | |
598 | /** |
599 | * Display file name and line number. |
600 | * @param nthStack call stack line where to find test file name and line number |
601 | */ |
602 | void writeLocation(int nthStack); |
603 | |
604 | void writeTestName(const QString &name); |
605 | void writeTypeAndProgram(const QString &type, const QString &program); |
606 | |
607 | void writeDataTest(bool sameInputOutput); |
608 | |
609 | bool checkMultiCursorCompatibility(const DocumentText &doc, bool blockSelection, QString *err); |
610 | |
611 | struct EditorConfig { |
612 | QString syntax; |
613 | QString indentationMode; |
614 | int indentationWidth; |
615 | int tabWidth; |
616 | bool replaceTabs; |
617 | bool autoBrackets; |
618 | |
619 | bool updated; |
620 | bool inherited; |
621 | }; |
622 | |
623 | static EditorConfig makeEditorConfig(); |
624 | |
625 | struct Config { |
626 | Placeholders fallbackPlaceholders; |
627 | Placeholders placeholders; |
628 | EditorConfig editorConfig; |
629 | }; |
630 | |
631 | QJSEngine *m_engine; |
632 | DocumentPrivate *m_doc; |
633 | ViewPrivate *m_view; |
634 | DocumentText m_input; |
635 | DocumentText m_output; |
636 | DocumentText m_expected; |
637 | Placeholders m_fallbackPlaceholders; |
638 | Placeholders m_defaultPlaceholders; |
639 | Placeholders m_placeholders; |
640 | EditorConfig m_editorConfig; |
641 | QTextStream m_stream; |
642 | QString m_debugMsg; |
643 | QString m_stringBuffer; |
644 | Format m_format; |
645 | Paths m_paths; |
646 | std::vector<Config> m_configStack; |
647 | QMap<QString, QString> m_libraryFiles; |
648 | TestExecutionConfig m_executionConfig; |
649 | DiffCommand m_diffCmd; |
650 | bool m_diffCmdLoaded = false; |
651 | bool m_hasDebugMessage = false; |
652 | int m_successCounter = 0; |
653 | int m_failureCounter = 0; |
654 | int m_xSuccessCounter = 0; |
655 | int m_xFailureCounter = 0; |
656 | int m_skipedCounter = 0; |
657 | int m_errorCounter = 0; |
658 | int m_breakOnErrorCounter = 0; |
659 | int m_dualModeAbortedCounter = 0; |
660 | }; |
661 | |
662 | Q_DECLARE_OPERATORS_FOR_FLAGS(ScriptTester::DebugOptions) |
663 | |
664 | } // namespace KTextEditor |
665 | |
666 | #endif |
667 | |