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
26class QJSEngine;
27
28namespace KTextEditor
29{
30
31class 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 */
46class KTEXTEDITOR_NO_EXPORT ScriptTester : public QObject
47{
48 Q_OBJECT
49
50public:
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
547private:
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
662Q_DECLARE_OPERATORS_FOR_FLAGS(ScriptTester::DebugOptions)
663
664} // namespace KTextEditor
665
666#endif
667

source code of ktexteditor/src/scripttester/scripttester_p.h