@git.zone/tstest 1.11.5 ā 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.js +24 -2
- package/dist_ts/tstest.classes.tap.parser.d.ts +10 -5
- package/dist_ts/tstest.classes.tap.parser.js +239 -99
- package/dist_ts/tstest.classes.tap.parser.old.d.ts +50 -0
- package/dist_ts/tstest.classes.tap.parser.old.js +332 -0
- package/dist_ts/tstest.classes.tstest.d.ts +1 -0
- package/dist_ts/tstest.classes.tstest.js +93 -4
- package/dist_ts/tstest.logging.d.ts +4 -0
- package/dist_ts/tstest.logging.js +36 -1
- package/dist_ts/tstest.plugins.d.ts +2 -1
- package/dist_ts/tstest.plugins.js +3 -2
- package/dist_ts_tapbundle/index.d.ts +1 -3
- package/dist_ts_tapbundle/index.js +3 -5
- package/dist_ts_tapbundle/tapbundle.classes.settingsmanager.d.ts +36 -0
- package/dist_ts_tapbundle/tapbundle.classes.settingsmanager.js +114 -0
- package/dist_ts_tapbundle/tapbundle.classes.tap.d.ts +26 -3
- package/dist_ts_tapbundle/tapbundle.classes.tap.js +218 -22
- package/dist_ts_tapbundle/tapbundle.classes.taptest.d.ts +5 -0
- package/dist_ts_tapbundle/tapbundle.classes.taptest.js +161 -7
- package/dist_ts_tapbundle/tapbundle.classes.taptools.d.ts +14 -0
- package/dist_ts_tapbundle/tapbundle.classes.taptools.js +24 -1
- package/dist_ts_tapbundle/tapbundle.expect.wrapper.d.ts +11 -0
- package/dist_ts_tapbundle/tapbundle.expect.wrapper.js +71 -0
- package/dist_ts_tapbundle/tapbundle.interfaces.d.ts +27 -0
- package/dist_ts_tapbundle/tapbundle.interfaces.js +2 -0
- package/dist_ts_tapbundle/tapbundle.utilities.diff.d.ts +5 -0
- package/dist_ts_tapbundle/tapbundle.utilities.diff.js +179 -0
- package/dist_ts_tapbundle_protocol/index.d.ts +6 -0
- package/dist_ts_tapbundle_protocol/index.js +11 -0
- package/dist_ts_tapbundle_protocol/protocol.emitter.d.ts +55 -0
- package/dist_ts_tapbundle_protocol/protocol.emitter.js +155 -0
- package/dist_ts_tapbundle_protocol/protocol.parser.d.ts +79 -0
- package/dist_ts_tapbundle_protocol/protocol.parser.js +359 -0
- package/dist_ts_tapbundle_protocol/protocol.types.d.ts +111 -0
- package/dist_ts_tapbundle_protocol/protocol.types.js +19 -0
- package/npmextra.json +3 -3
- package/package.json +2 -1
- package/readme.hints.md +166 -5
- package/readme.md +135 -4
- package/readme.plan.md +145 -56
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +22 -1
- package/ts/tspublish.json +1 -1
- package/ts/tstest.classes.tap.parser.ts +271 -110
- package/ts/tstest.classes.tstest.ts +112 -5
- package/ts/tstest.logging.ts +43 -0
- package/ts/tstest.plugins.ts +2 -0
|
@@ -8,28 +8,27 @@ import * as plugins from './tstest.plugins.js';
|
|
|
8
8
|
import { TapTestResult } from './tstest.classes.tap.testresult.js';
|
|
9
9
|
import * as logPrefixes from './tstest.logprefixes.js';
|
|
10
10
|
import { TsTestLogger } from './tstest.logging.js';
|
|
11
|
+
import { ProtocolParser } from '../dist_ts_tapbundle_protocol/index.js';
|
|
12
|
+
import type { IProtocolMessage, ITestResult, IPlanLine, IErrorBlock, ITestEvent } from '../dist_ts_tapbundle_protocol/index.js';
|
|
11
13
|
|
|
12
14
|
export class TapParser {
|
|
13
15
|
testStore: TapTestResult[] = [];
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
receivedTests: number;
|
|
17
|
+
expectedTests: number = 0;
|
|
18
|
+
receivedTests: number = 0;
|
|
18
19
|
|
|
19
|
-
testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*?)(\s#\s(.*))?$/;
|
|
20
20
|
activeTapTestResult: TapTestResult;
|
|
21
|
-
collectingErrorDetails: boolean = false;
|
|
22
|
-
currentTestError: string[] = [];
|
|
23
|
-
|
|
24
|
-
pretaskRegex = /^::__PRETASK:(.*)$/;
|
|
25
21
|
|
|
26
22
|
private logger: TsTestLogger;
|
|
23
|
+
private protocolParser: ProtocolParser;
|
|
24
|
+
private protocolVersion: string | null = null;
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
27
|
* the constructor for TapParser
|
|
30
28
|
*/
|
|
31
29
|
constructor(public fileName: string, logger?: TsTestLogger) {
|
|
32
30
|
this.logger = logger;
|
|
31
|
+
this.protocolParser = new ProtocolParser();
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
@@ -75,137 +74,299 @@ export class TapParser {
|
|
|
75
74
|
logLineArray.pop();
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
//
|
|
77
|
+
// Process each line through the protocol parser
|
|
79
78
|
for (const logLine of logLineArray) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// initiating first TapResult
|
|
90
|
-
this._getNewTapTestResult();
|
|
91
|
-
} else if (this.pretaskRegex.test(logLine)) {
|
|
92
|
-
logLineIsTapProtocol = true;
|
|
93
|
-
const pretaskContentMatch = this.pretaskRegex.exec(logLine);
|
|
94
|
-
if (pretaskContentMatch && pretaskContentMatch[1]) {
|
|
95
|
-
if (this.logger) {
|
|
96
|
-
this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} else if (this.testStatusRegex.test(logLine)) {
|
|
100
|
-
logLineIsTapProtocol = true;
|
|
101
|
-
const regexResult = this.testStatusRegex.exec(logLine);
|
|
102
|
-
// const testId = parseInt(regexResult[2]); // Currently unused
|
|
103
|
-
const testOk = (() => {
|
|
104
|
-
if (regexResult[1] === 'ok') {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
})();
|
|
109
|
-
|
|
110
|
-
const testSubject = regexResult[3].trim();
|
|
111
|
-
const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
|
|
112
|
-
|
|
113
|
-
let testDuration = 0;
|
|
114
|
-
|
|
115
|
-
if (testMetadata) {
|
|
116
|
-
const timeMatch = testMetadata.match(/time=(\d+)ms/);
|
|
117
|
-
// const skipMatch = testMetadata.match(/SKIP\s*(.*)/); // Currently unused
|
|
118
|
-
// const todoMatch = testMetadata.match(/TODO\s*(.*)/); // Currently unused
|
|
119
|
-
|
|
120
|
-
if (timeMatch) {
|
|
121
|
-
testDuration = parseInt(timeMatch[1]);
|
|
122
|
-
}
|
|
123
|
-
// Skip/todo handling could be added here in the future
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// test for protocol error - disabled as it's not critical
|
|
127
|
-
// The test ID mismatch can occur when tests are filtered, skipped, or use todo
|
|
128
|
-
// if (testId !== this.activeTapTestResult.id) {
|
|
129
|
-
// if (this.logger) {
|
|
130
|
-
// this.logger.error('Something is strange! Test Ids are not equal!');
|
|
131
|
-
// }
|
|
132
|
-
// }
|
|
133
|
-
this.activeTapTestResult.setTestResult(testOk);
|
|
134
|
-
|
|
135
|
-
if (testOk) {
|
|
136
|
-
if (this.logger) {
|
|
137
|
-
this.logger.testResult(testSubject, true, testDuration);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
// Start collecting error details for failed test
|
|
141
|
-
this.collectingErrorDetails = true;
|
|
142
|
-
this.currentTestError = [];
|
|
143
|
-
if (this.logger) {
|
|
144
|
-
this.logger.testResult(testSubject, false, testDuration);
|
|
145
|
-
}
|
|
79
|
+
const messages = this.protocolParser.parseLine(logLine);
|
|
80
|
+
|
|
81
|
+
if (messages.length > 0) {
|
|
82
|
+
// Handle protocol messages
|
|
83
|
+
for (const message of messages) {
|
|
84
|
+
this._handleProtocolMessage(message, logLine);
|
|
146
85
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!logLineIsTapProtocol) {
|
|
86
|
+
} else {
|
|
87
|
+
// Not a protocol message, handle as console output
|
|
150
88
|
if (this.activeTapTestResult) {
|
|
151
89
|
this.activeTapTestResult.addLogLine(logLine);
|
|
152
90
|
}
|
|
153
91
|
|
|
154
|
-
// Check for snapshot communication
|
|
92
|
+
// Check for snapshot communication (legacy)
|
|
155
93
|
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
|
|
156
94
|
if (snapshotMatch) {
|
|
157
95
|
const base64Data = snapshotMatch[1];
|
|
158
96
|
try {
|
|
159
97
|
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
|
|
160
98
|
this.handleSnapshot(snapshotData);
|
|
161
|
-
} catch (error) {
|
|
99
|
+
} catch (error: any) {
|
|
162
100
|
if (this.logger) {
|
|
163
101
|
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
|
164
102
|
}
|
|
165
103
|
}
|
|
104
|
+
} else if (this.logger) {
|
|
105
|
+
// This is console output from the test file
|
|
106
|
+
this.logger.testConsoleOutput(logLine);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private _handleProtocolMessage(message: IProtocolMessage, originalLine: string) {
|
|
113
|
+
switch (message.type) {
|
|
114
|
+
case 'protocol':
|
|
115
|
+
this.protocolVersion = message.content.version;
|
|
116
|
+
if (this.logger) {
|
|
117
|
+
this.logger.tapOutput(`Protocol version: ${this.protocolVersion}`);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'version':
|
|
122
|
+
// TAP version, we can ignore this
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'plan':
|
|
126
|
+
const plan = message.content as IPlanLine;
|
|
127
|
+
this.expectedTests = plan.end - plan.start + 1;
|
|
128
|
+
if (plan.skipAll) {
|
|
129
|
+
if (this.logger) {
|
|
130
|
+
this.logger.tapOutput(`Skipping all tests: ${plan.skipAll}`);
|
|
131
|
+
}
|
|
166
132
|
} else {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Check if this line is an error detail (starts with Error: or has stack trace characteristics)
|
|
170
|
-
if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
|
|
171
|
-
this.currentTestError.push(logLine);
|
|
172
|
-
} else if (this.currentTestError.length > 0) {
|
|
173
|
-
// End of error details, show the error
|
|
174
|
-
const errorMessage = this.currentTestError.join('\n');
|
|
175
|
-
if (this.logger) {
|
|
176
|
-
this.logger.testErrorDetails(errorMessage);
|
|
177
|
-
}
|
|
178
|
-
this.collectingErrorDetails = false;
|
|
179
|
-
this.currentTestError = [];
|
|
180
|
-
}
|
|
133
|
+
if (this.logger) {
|
|
134
|
+
this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
|
|
181
135
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
136
|
+
}
|
|
137
|
+
// Initialize first TapResult
|
|
138
|
+
this._getNewTapTestResult();
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'test':
|
|
142
|
+
const testResult = message.content as ITestResult;
|
|
143
|
+
|
|
144
|
+
// Update active test result
|
|
145
|
+
this.activeTapTestResult.setTestResult(testResult.ok);
|
|
146
|
+
|
|
147
|
+
// Extract test duration from metadata
|
|
148
|
+
let testDuration = 0;
|
|
149
|
+
if (testResult.metadata?.time) {
|
|
150
|
+
testDuration = testResult.metadata.time;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Log test result
|
|
154
|
+
if (this.logger) {
|
|
155
|
+
if (testResult.ok) {
|
|
156
|
+
this.logger.testResult(testResult.description, true, testDuration);
|
|
157
|
+
} else {
|
|
158
|
+
this.logger.testResult(testResult.description, false, testDuration);
|
|
159
|
+
|
|
160
|
+
// If there's error metadata, show it
|
|
161
|
+
if (testResult.metadata?.error) {
|
|
162
|
+
const error = testResult.metadata.error;
|
|
163
|
+
let errorDetails = error.message;
|
|
164
|
+
if (error.stack) {
|
|
165
|
+
errorDetails = error.stack;
|
|
166
|
+
}
|
|
167
|
+
this.logger.testErrorDetails(errorDetails);
|
|
188
168
|
}
|
|
189
169
|
}
|
|
190
170
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Ensure any pending error is shown before settling the test
|
|
195
|
-
if (this.collectingErrorDetails && this.currentTestError.length > 0) {
|
|
196
|
-
const errorMessage = this.currentTestError.join('\n');
|
|
171
|
+
|
|
172
|
+
// Handle directives (skip/todo)
|
|
173
|
+
if (testResult.directive) {
|
|
197
174
|
if (this.logger) {
|
|
198
|
-
|
|
175
|
+
if (testResult.directive.type === 'skip') {
|
|
176
|
+
this.logger.testConsoleOutput(`Test skipped: ${testResult.directive.reason || 'No reason given'}`);
|
|
177
|
+
} else if (testResult.directive.type === 'todo') {
|
|
178
|
+
this.logger.testConsoleOutput(`Test todo: ${testResult.directive.reason || 'No reason given'}`);
|
|
179
|
+
}
|
|
199
180
|
}
|
|
200
|
-
this.collectingErrorDetails = false;
|
|
201
|
-
this.currentTestError = [];
|
|
202
181
|
}
|
|
203
182
|
|
|
183
|
+
// Mark test as settled and move to next
|
|
184
|
+
this.activeTapTestResult.testSettled = true;
|
|
204
185
|
this.testStore.push(this.activeTapTestResult);
|
|
205
186
|
this._getNewTapTestResult();
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'comment':
|
|
190
|
+
if (this.logger) {
|
|
191
|
+
// Check if it's a pretask comment
|
|
192
|
+
const pretaskMatch = message.content.match(/^Pretask -> (.+): Success\.$/);
|
|
193
|
+
if (pretaskMatch) {
|
|
194
|
+
this.logger.tapOutput(message.content);
|
|
195
|
+
} else {
|
|
196
|
+
this.logger.testConsoleOutput(message.content);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'bailout':
|
|
202
|
+
if (this.logger) {
|
|
203
|
+
this.logger.error(`Bail out! ${message.content}`);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'error':
|
|
208
|
+
const errorBlock = message.content as IErrorBlock;
|
|
209
|
+
if (this.logger && errorBlock.error) {
|
|
210
|
+
let errorDetails = errorBlock.error.message;
|
|
211
|
+
if (errorBlock.error.stack) {
|
|
212
|
+
errorDetails = errorBlock.error.stack;
|
|
213
|
+
}
|
|
214
|
+
this.logger.testErrorDetails(errorDetails);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'snapshot':
|
|
219
|
+
// Handle new protocol snapshot format
|
|
220
|
+
const snapshot = message.content;
|
|
221
|
+
this.handleSnapshot({
|
|
222
|
+
path: snapshot.name,
|
|
223
|
+
content: typeof snapshot.content === 'string' ? snapshot.content : JSON.stringify(snapshot.content),
|
|
224
|
+
action: 'compare' // Default action
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'event':
|
|
229
|
+
const event = message.content as ITestEvent;
|
|
230
|
+
this._handleTestEvent(event);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private _handleTestEvent(event: ITestEvent) {
|
|
236
|
+
if (!this.logger) return;
|
|
237
|
+
|
|
238
|
+
switch (event.eventType) {
|
|
239
|
+
case 'test:queued':
|
|
240
|
+
// We can track queued tests if needed
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case 'test:started':
|
|
244
|
+
this.logger.testConsoleOutput(cs(`Test starting: ${event.data.description}`, 'cyan'));
|
|
245
|
+
if (event.data.retry) {
|
|
246
|
+
this.logger.testConsoleOutput(cs(` Retry attempt ${event.data.retry}`, 'orange'));
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'test:progress':
|
|
251
|
+
if (event.data.progress !== undefined) {
|
|
252
|
+
this.logger.testConsoleOutput(cs(` Progress: ${event.data.progress}%`, 'cyan'));
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'test:completed':
|
|
257
|
+
// Test completion is already handled by the test result
|
|
258
|
+
// This event provides additional timing info if needed
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case 'suite:started':
|
|
262
|
+
this.logger.testConsoleOutput(cs(`\nSuite: ${event.data.suiteName}`, 'blue'));
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'suite:completed':
|
|
266
|
+
this.logger.testConsoleOutput(cs(`Suite completed: ${event.data.suiteName}\n`, 'blue'));
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'hook:started':
|
|
270
|
+
this.logger.testConsoleOutput(cs(` Hook: ${event.data.hookName}`, 'cyan'));
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'hook:completed':
|
|
274
|
+
// Silent unless there's an error
|
|
275
|
+
if (event.data.error) {
|
|
276
|
+
this.logger.testConsoleOutput(cs(` Hook failed: ${event.data.hookName}`, 'red'));
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case 'assertion:failed':
|
|
281
|
+
// Enhanced assertion failure with diff
|
|
282
|
+
if (event.data.error) {
|
|
283
|
+
this._displayAssertionError(event.data.error);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _displayAssertionError(error: any) {
|
|
290
|
+
if (!this.logger) return;
|
|
291
|
+
|
|
292
|
+
// Display error message
|
|
293
|
+
if (error.message) {
|
|
294
|
+
this.logger.testErrorDetails(error.message);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Display visual diff if available
|
|
298
|
+
if (error.diff) {
|
|
299
|
+
this._displayDiff(error.diff, error.expected, error.actual);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private _displayDiff(diff: any, expected: any, actual: any) {
|
|
304
|
+
if (!this.logger) return;
|
|
305
|
+
|
|
306
|
+
this.logger.testConsoleOutput(cs('\n Diff:', 'cyan'));
|
|
307
|
+
|
|
308
|
+
switch (diff.type) {
|
|
309
|
+
case 'string':
|
|
310
|
+
this._displayStringDiff(diff.changes);
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'object':
|
|
314
|
+
this._displayObjectDiff(diff.changes, expected, actual);
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'array':
|
|
318
|
+
this._displayArrayDiff(diff.changes, expected, actual);
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'primitive':
|
|
322
|
+
this._displayPrimitiveDiff(diff.changes);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private _displayStringDiff(changes: any[]) {
|
|
328
|
+
for (const change of changes) {
|
|
329
|
+
const linePrefix = ` Line ${change.line + 1}: `;
|
|
330
|
+
if (change.type === 'add') {
|
|
331
|
+
this.logger.testConsoleOutput(cs(`${linePrefix}+ ${change.content}`, 'green'));
|
|
332
|
+
} else if (change.type === 'remove') {
|
|
333
|
+
this.logger.testConsoleOutput(cs(`${linePrefix}- ${change.content}`, 'red'));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private _displayObjectDiff(changes: any[], expected: any, actual: any) {
|
|
339
|
+
this.logger.testConsoleOutput(cs(' Expected:', 'red'));
|
|
340
|
+
this.logger.testConsoleOutput(` ${JSON.stringify(expected, null, 2)}`);
|
|
341
|
+
this.logger.testConsoleOutput(cs(' Actual:', 'green'));
|
|
342
|
+
this.logger.testConsoleOutput(` ${JSON.stringify(actual, null, 2)}`);
|
|
343
|
+
|
|
344
|
+
this.logger.testConsoleOutput(cs('\n Changes:', 'cyan'));
|
|
345
|
+
for (const change of changes) {
|
|
346
|
+
const path = change.path.join('.');
|
|
347
|
+
if (change.type === 'add') {
|
|
348
|
+
this.logger.testConsoleOutput(cs(` + ${path}: ${JSON.stringify(change.newValue)}`, 'green'));
|
|
349
|
+
} else if (change.type === 'remove') {
|
|
350
|
+
this.logger.testConsoleOutput(cs(` - ${path}: ${JSON.stringify(change.oldValue)}`, 'red'));
|
|
351
|
+
} else if (change.type === 'modify') {
|
|
352
|
+
this.logger.testConsoleOutput(cs(` ~ ${path}:`, 'cyan'));
|
|
353
|
+
this.logger.testConsoleOutput(cs(` - ${JSON.stringify(change.oldValue)}`, 'red'));
|
|
354
|
+
this.logger.testConsoleOutput(cs(` + ${JSON.stringify(change.newValue)}`, 'green'));
|
|
206
355
|
}
|
|
207
356
|
}
|
|
208
357
|
}
|
|
358
|
+
|
|
359
|
+
private _displayArrayDiff(changes: any[], expected: any[], actual: any[]) {
|
|
360
|
+
this._displayObjectDiff(changes, expected, actual);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private _displayPrimitiveDiff(changes: any[]) {
|
|
364
|
+
const change = changes[0];
|
|
365
|
+
if (change) {
|
|
366
|
+
this.logger.testConsoleOutput(cs(` Expected: ${JSON.stringify(change.oldValue)}`, 'red'));
|
|
367
|
+
this.logger.testConsoleOutput(cs(` Actual: ${JSON.stringify(change.newValue)}`, 'green'));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
209
370
|
|
|
210
371
|
/**
|
|
211
372
|
* returns all tests that are not completed
|
|
@@ -353,4 +514,4 @@ export class TapParser {
|
|
|
353
514
|
}
|
|
354
515
|
}
|
|
355
516
|
}
|
|
356
|
-
}
|
|
517
|
+
}
|
|
@@ -101,6 +101,77 @@ export class TsTest {
|
|
|
101
101
|
tapCombinator.evaluate();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
public async runWatch(ignorePatterns: string[] = []) {
|
|
105
|
+
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
|
|
106
|
+
|
|
107
|
+
console.clear();
|
|
108
|
+
this.logger.watchModeStart();
|
|
109
|
+
|
|
110
|
+
// Initial run
|
|
111
|
+
await this.run();
|
|
112
|
+
|
|
113
|
+
// Set up file watcher
|
|
114
|
+
const fileChanges = new Map<string, NodeJS.Timeout>();
|
|
115
|
+
const debounceTime = 300; // 300ms debounce
|
|
116
|
+
|
|
117
|
+
const runTestsAfterChange = async () => {
|
|
118
|
+
console.clear();
|
|
119
|
+
const changedFiles = Array.from(fileChanges.keys());
|
|
120
|
+
fileChanges.clear();
|
|
121
|
+
|
|
122
|
+
this.logger.watchModeRerun(changedFiles);
|
|
123
|
+
await this.run();
|
|
124
|
+
this.logger.watchModeWaiting();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Start watching before subscribing to events
|
|
128
|
+
await smartchokInstance.start();
|
|
129
|
+
|
|
130
|
+
// Subscribe to file change events
|
|
131
|
+
const changeObservable = await smartchokInstance.getObservableFor('change');
|
|
132
|
+
const addObservable = await smartchokInstance.getObservableFor('add');
|
|
133
|
+
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
|
|
134
|
+
|
|
135
|
+
const handleFileChange = (changedPath: string) => {
|
|
136
|
+
// Skip if path matches ignore patterns
|
|
137
|
+
if (ignorePatterns.some(pattern => changedPath.includes(pattern))) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Clear existing timeout for this file if any
|
|
142
|
+
if (fileChanges.has(changedPath)) {
|
|
143
|
+
clearTimeout(fileChanges.get(changedPath));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set new timeout for this file
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
fileChanges.delete(changedPath);
|
|
149
|
+
if (fileChanges.size === 0) {
|
|
150
|
+
runTestsAfterChange();
|
|
151
|
+
}
|
|
152
|
+
}, debounceTime);
|
|
153
|
+
|
|
154
|
+
fileChanges.set(changedPath, timeout);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Subscribe to all relevant events
|
|
158
|
+
changeObservable.subscribe(([path]) => handleFileChange(path));
|
|
159
|
+
addObservable.subscribe(([path]) => handleFileChange(path));
|
|
160
|
+
unlinkObservable.subscribe(([path]) => handleFileChange(path));
|
|
161
|
+
|
|
162
|
+
this.logger.watchModeWaiting();
|
|
163
|
+
|
|
164
|
+
// Handle Ctrl+C to exit gracefully
|
|
165
|
+
process.on('SIGINT', async () => {
|
|
166
|
+
this.logger.watchModeStop();
|
|
167
|
+
await smartchokInstance.stop();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Keep the process running
|
|
172
|
+
await new Promise(() => {}); // This promise never resolves
|
|
173
|
+
}
|
|
174
|
+
|
|
104
175
|
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
|
105
176
|
// Check if this file should be skipped based on range
|
|
106
177
|
if (this.startFromFile !== null && fileIndex < this.startFromFile) {
|
|
@@ -161,9 +232,45 @@ export class TsTest {
|
|
|
161
232
|
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
162
233
|
}
|
|
163
234
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
235
|
+
// Check for 00init.ts file in test directory
|
|
236
|
+
const testDir = plugins.path.dirname(fileNameArg);
|
|
237
|
+
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
238
|
+
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
|
|
239
|
+
|
|
240
|
+
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
|
241
|
+
|
|
242
|
+
// If 00init.ts exists, run it first
|
|
243
|
+
if (initFileExists) {
|
|
244
|
+
// Create a temporary loader file that imports both 00init.ts and the test file
|
|
245
|
+
const absoluteInitFile = plugins.path.resolve(initFile);
|
|
246
|
+
const absoluteTestFile = plugins.path.resolve(fileNameArg);
|
|
247
|
+
const loaderContent = `
|
|
248
|
+
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
249
|
+
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
250
|
+
`;
|
|
251
|
+
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
|
252
|
+
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
|
253
|
+
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
|
257
|
+
|
|
258
|
+
// If we created a loader file, clean it up after test execution
|
|
259
|
+
if (initFileExists) {
|
|
260
|
+
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
try {
|
|
263
|
+
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
|
264
|
+
plugins.smartfile.fs.removeSync(loaderPath);
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// Ignore cleanup errors
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
execResultStreaming.childProcess.on('exit', cleanup);
|
|
272
|
+
execResultStreaming.childProcess.on('error', cleanup);
|
|
273
|
+
}
|
|
167
274
|
|
|
168
275
|
// Handle timeout if specified
|
|
169
276
|
if (this.timeoutSeconds !== null) {
|
|
@@ -382,10 +489,10 @@ export class TsTest {
|
|
|
382
489
|
try {
|
|
383
490
|
// Delete 00err and 00diff directories if they exist
|
|
384
491
|
if (await plugins.smartfile.fs.isDirectory(errDir)) {
|
|
385
|
-
|
|
492
|
+
plugins.smartfile.fs.removeSync(errDir);
|
|
386
493
|
}
|
|
387
494
|
if (await plugins.smartfile.fs.isDirectory(diffDir)) {
|
|
388
|
-
|
|
495
|
+
plugins.smartfile.fs.removeSync(diffDir);
|
|
389
496
|
}
|
|
390
497
|
|
|
391
498
|
// Get all .log files in log directory (not in subdirectories)
|
package/ts/tstest.logging.ts
CHANGED
|
@@ -520,4 +520,47 @@ export class TsTestLogger {
|
|
|
520
520
|
|
|
521
521
|
return diff;
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
// Watch mode methods
|
|
525
|
+
watchModeStart() {
|
|
526
|
+
if (this.options.json) {
|
|
527
|
+
this.logJson({ event: 'watchModeStart' });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.log(this.format('\nš Watch Mode', 'cyan'));
|
|
532
|
+
this.log(this.format(' Running tests in watch mode...', 'dim'));
|
|
533
|
+
this.log(this.format(' Press Ctrl+C to exit\n', 'dim'));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
watchModeWaiting() {
|
|
537
|
+
if (this.options.json) {
|
|
538
|
+
this.logJson({ event: 'watchModeWaiting' });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.log(this.format('\n Waiting for file changes...', 'dim'));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
watchModeRerun(changedFiles: string[]) {
|
|
546
|
+
if (this.options.json) {
|
|
547
|
+
this.logJson({ event: 'watchModeRerun', changedFiles });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this.log(this.format('\nš File changes detected:', 'cyan'));
|
|
552
|
+
changedFiles.forEach(file => {
|
|
553
|
+
this.log(this.format(` ⢠${file}`, 'yellow'));
|
|
554
|
+
});
|
|
555
|
+
this.log(this.format('\n Re-running tests...\n', 'dim'));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
watchModeStop() {
|
|
559
|
+
if (this.options.json) {
|
|
560
|
+
this.logJson({ event: 'watchModeStop' });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this.log(this.format('\n\nš Stopping watch mode...', 'cyan'));
|
|
565
|
+
}
|
|
523
566
|
}
|
package/ts/tstest.plugins.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
// @push.rocks scope
|
|
14
14
|
import * as consolecolor from '@push.rocks/consolecolor';
|
|
15
15
|
import * as smartbrowser from '@push.rocks/smartbrowser';
|
|
16
|
+
import * as smartchok from '@push.rocks/smartchok';
|
|
16
17
|
import * as smartdelay from '@push.rocks/smartdelay';
|
|
17
18
|
import * as smartfile from '@push.rocks/smartfile';
|
|
18
19
|
import * as smartlog from '@push.rocks/smartlog';
|
|
@@ -23,6 +24,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
|
|
23
24
|
export {
|
|
24
25
|
consolecolor,
|
|
25
26
|
smartbrowser,
|
|
27
|
+
smartchok,
|
|
26
28
|
smartdelay,
|
|
27
29
|
smartfile,
|
|
28
30
|
smartlog,
|