@git.zone/tstest 1.5.0 → 1.8.0

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.
Files changed (68) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/index.js +9 -2
  3. package/dist_ts/tstest.classes.tap.parser.d.ts +4 -0
  4. package/dist_ts/tstest.classes.tap.parser.js +114 -24
  5. package/dist_ts/tstest.classes.testdirectory.d.ts +10 -0
  6. package/dist_ts/tstest.classes.testdirectory.js +31 -1
  7. package/dist_ts/tstest.classes.tstest.d.ts +3 -1
  8. package/dist_ts/tstest.classes.tstest.js +52 -27
  9. package/dist_ts_tapbundle/index.d.ts +1 -0
  10. package/dist_ts_tapbundle/index.js +2 -1
  11. package/dist_ts_tapbundle/tapbundle.classes.tap.d.ts +54 -1
  12. package/dist_ts_tapbundle/tapbundle.classes.tap.js +288 -24
  13. package/dist_ts_tapbundle/tapbundle.classes.taptest.d.ts +7 -1
  14. package/dist_ts_tapbundle/tapbundle.classes.taptest.js +75 -27
  15. package/dist_ts_tapbundle/tapbundle.classes.taptools.d.ts +81 -1
  16. package/dist_ts_tapbundle/tapbundle.classes.taptools.js +180 -2
  17. package/dist_ts_tapbundle/ts_tapbundle/00_commitinfo_data.d.ts +8 -0
  18. package/dist_ts_tapbundle/ts_tapbundle/00_commitinfo_data.js +9 -0
  19. package/dist_ts_tapbundle/ts_tapbundle/index.d.ts +6 -0
  20. package/dist_ts_tapbundle/ts_tapbundle/index.js +7 -0
  21. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.pretask.d.ts +10 -0
  22. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.pretask.js +13 -0
  23. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.tap.d.ts +104 -0
  24. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.tap.js +401 -0
  25. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.taptest.d.ts +38 -0
  26. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.taptest.js +110 -0
  27. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.taptools.d.ts +109 -0
  28. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.taptools.js +241 -0
  29. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.tapwrap.d.ts +8 -0
  30. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.classes.tapwrap.js +7 -0
  31. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.plugins.d.ts +8 -0
  32. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.plugins.js +10 -0
  33. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.tapcreator.d.ts +3 -0
  34. package/dist_ts_tapbundle/ts_tapbundle/tapbundle.tapcreator.js +5 -0
  35. package/dist_ts_tapbundle/ts_tapbundle/webhelpers.d.ts +7 -0
  36. package/dist_ts_tapbundle/ts_tapbundle/webhelpers.js +35 -0
  37. package/dist_ts_tapbundle/ts_tapbundle_node/classes.pathinject.d.ts +5 -0
  38. package/dist_ts_tapbundle/ts_tapbundle_node/classes.pathinject.js +13 -0
  39. package/dist_ts_tapbundle/ts_tapbundle_node/plugins.d.ts +11 -0
  40. package/dist_ts_tapbundle/ts_tapbundle_node/plugins.js +14 -0
  41. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.classes.taptest.d.ts +38 -0
  42. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.classes.taptest.js +110 -0
  43. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.classes.taptools.d.ts +109 -0
  44. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.classes.taptools.js +241 -0
  45. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.plugins.d.ts +8 -0
  46. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.plugins.js +10 -0
  47. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.tapcreator.d.ts +3 -0
  48. package/dist_ts_tapbundle_node/ts_tapbundle/tapbundle.tapcreator.js +5 -0
  49. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.pathinject.d.ts +5 -0
  50. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.pathinject.js +13 -0
  51. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.tapnodetools.d.ts +25 -0
  52. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.tapnodetools.js +81 -0
  53. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.testfileprovider.d.ts +6 -0
  54. package/dist_ts_tapbundle_node/ts_tapbundle_node/classes.testfileprovider.js +16 -0
  55. package/dist_ts_tapbundle_node/ts_tapbundle_node/index.d.ts +2 -0
  56. package/dist_ts_tapbundle_node/ts_tapbundle_node/index.js +3 -0
  57. package/dist_ts_tapbundle_node/ts_tapbundle_node/paths.d.ts +2 -0
  58. package/dist_ts_tapbundle_node/ts_tapbundle_node/paths.js +4 -0
  59. package/dist_ts_tapbundle_node/ts_tapbundle_node/plugins.d.ts +11 -0
  60. package/dist_ts_tapbundle_node/ts_tapbundle_node/plugins.js +14 -0
  61. package/package.json +11 -8
  62. package/readme.md +141 -0
  63. package/readme.plan.md +253 -30
  64. package/ts/00_commitinfo_data.ts +1 -1
  65. package/ts/index.ts +8 -1
  66. package/ts/tstest.classes.tap.parser.ts +111 -25
  67. package/ts/tstest.classes.testdirectory.ts +39 -0
  68. package/ts/tstest.classes.tstest.ts +61 -27
@@ -16,7 +16,7 @@ export class TapParser {
16
16
  expectedTests: number;
17
17
  receivedTests: number;
18
18
 
19
- testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*)\s#\stime=(.*)ms$/;
19
+ testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*)(\s#\s(.*))?$/;
20
20
  activeTapTestResult: TapTestResult;
21
21
  collectingErrorDetails: boolean = false;
22
22
  currentTestError: string[] = [];
@@ -78,14 +78,33 @@ export class TapParser {
78
78
  })();
79
79
 
80
80
  const testSubject = regexResult[3];
81
- const testDuration = parseInt(regexResult[4]);
82
-
83
- // test for protocol error
84
- if (testId !== this.activeTapTestResult.id) {
85
- if (this.logger) {
86
- this.logger.error('Something is strange! Test Ids are not equal!');
81
+ const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
82
+
83
+ let testDuration = 0;
84
+ let isSkipped = false;
85
+ let isTodo = false;
86
+
87
+ if (testMetadata) {
88
+ const timeMatch = testMetadata.match(/time=(\d+)ms/);
89
+ const skipMatch = testMetadata.match(/SKIP\s*(.*)/);
90
+ const todoMatch = testMetadata.match(/TODO\s*(.*)/);
91
+
92
+ if (timeMatch) {
93
+ testDuration = parseInt(timeMatch[1]);
94
+ } else if (skipMatch) {
95
+ isSkipped = true;
96
+ } else if (todoMatch) {
97
+ isTodo = true;
87
98
  }
88
99
  }
100
+
101
+ // test for protocol error - disabled as it's not critical
102
+ // The test ID mismatch can occur when tests are filtered, skipped, or use todo
103
+ // if (testId !== this.activeTapTestResult.id) {
104
+ // if (this.logger) {
105
+ // this.logger.error('Something is strange! Test Ids are not equal!');
106
+ // }
107
+ // }
89
108
  this.activeTapTestResult.setTestResult(testOk);
90
109
 
91
110
  if (testOk) {
@@ -107,27 +126,41 @@ export class TapParser {
107
126
  this.activeTapTestResult.addLogLine(logLine);
108
127
  }
109
128
 
110
- // Check if we're collecting error details
111
- if (this.collectingErrorDetails) {
112
- // Check if this line is an error detail (starts with Error: or has stack trace characteristics)
113
- if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
114
- this.currentTestError.push(logLine);
115
- } else if (this.currentTestError.length > 0) {
116
- // End of error details, show the error
117
- const errorMessage = this.currentTestError.join('\n');
129
+ // Check for snapshot communication
130
+ const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
131
+ if (snapshotMatch) {
132
+ const base64Data = snapshotMatch[1];
133
+ try {
134
+ const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
135
+ this.handleSnapshot(snapshotData);
136
+ } catch (error) {
118
137
  if (this.logger) {
119
- this.logger.testErrorDetails(errorMessage);
138
+ this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
120
139
  }
121
- this.collectingErrorDetails = false;
122
- this.currentTestError = [];
123
140
  }
124
- }
125
-
126
- // Don't output TAP error details as console output when we're collecting them
127
- if (!this.collectingErrorDetails || (!logLine.trim().startsWith('Error:') && !logLine.trim().match(/^\s*at\s/))) {
128
- if (this.logger) {
129
- // This is console output from the test file, not TAP protocol
130
- this.logger.testConsoleOutput(logLine);
141
+ } else {
142
+ // Check if we're collecting error details
143
+ if (this.collectingErrorDetails) {
144
+ // Check if this line is an error detail (starts with Error: or has stack trace characteristics)
145
+ if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
146
+ this.currentTestError.push(logLine);
147
+ } else if (this.currentTestError.length > 0) {
148
+ // End of error details, show the error
149
+ const errorMessage = this.currentTestError.join('\n');
150
+ if (this.logger) {
151
+ this.logger.testErrorDetails(errorMessage);
152
+ }
153
+ this.collectingErrorDetails = false;
154
+ this.currentTestError = [];
155
+ }
156
+ }
157
+
158
+ // Don't output TAP error details as console output when we're collecting them
159
+ if (!this.collectingErrorDetails || (!logLine.trim().startsWith('Error:') && !logLine.trim().match(/^\s*at\s/))) {
160
+ if (this.logger) {
161
+ // This is console output from the test file, not TAP protocol
162
+ this.logger.testConsoleOutput(logLine);
163
+ }
131
164
  }
132
165
  }
133
166
  }
@@ -205,6 +238,59 @@ export class TapParser {
205
238
  public async handleTapLog(tapLog: string) {
206
239
  this._processLog(tapLog);
207
240
  }
241
+
242
+ /**
243
+ * Handle snapshot data from the test
244
+ */
245
+ private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
246
+ try {
247
+ const smartfile = await import('@push.rocks/smartfile');
248
+
249
+ if (snapshotData.action === 'compare') {
250
+ // Try to read existing snapshot
251
+ try {
252
+ const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
253
+ if (existingSnapshot !== snapshotData.content) {
254
+ // Snapshot mismatch
255
+ if (this.logger) {
256
+ this.logger.testConsoleOutput(`Snapshot mismatch: ${snapshotData.path}`);
257
+ this.logger.testConsoleOutput(`Expected:\n${existingSnapshot}`);
258
+ this.logger.testConsoleOutput(`Received:\n${snapshotData.content}`);
259
+ }
260
+ // TODO: Communicate failure back to the test
261
+ } else {
262
+ if (this.logger) {
263
+ this.logger.testConsoleOutput(`Snapshot matched: ${snapshotData.path}`);
264
+ }
265
+ }
266
+ } catch (error: any) {
267
+ if (error.code === 'ENOENT') {
268
+ // Snapshot doesn't exist, create it
269
+ const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
270
+ await smartfile.fs.ensureDir(dirPath);
271
+ await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
272
+ if (this.logger) {
273
+ this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
274
+ }
275
+ } else {
276
+ throw error;
277
+ }
278
+ }
279
+ } else if (snapshotData.action === 'update') {
280
+ // Update snapshot
281
+ const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
282
+ await smartfile.fs.ensureDir(dirPath);
283
+ await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
284
+ if (this.logger) {
285
+ this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
286
+ }
287
+ }
288
+ } catch (error: any) {
289
+ if (this.logger) {
290
+ this.logger.testConsoleOutput(`Error handling snapshot: ${error.message}`);
291
+ }
292
+ }
293
+ }
208
294
 
209
295
  public async evaluateFinalResult() {
210
296
  this.receivedTests = this.testStore.length;
@@ -99,4 +99,43 @@ export class TestDirectory {
99
99
  }
100
100
  return testFilePaths;
101
101
  }
102
+
103
+ /**
104
+ * Get test files organized by parallel execution groups
105
+ * @returns An object with grouped tests
106
+ */
107
+ async getTestFileGroups(): Promise<{
108
+ serial: string[];
109
+ parallelGroups: { [groupName: string]: string[] };
110
+ }> {
111
+ await this._init();
112
+
113
+ const result = {
114
+ serial: [] as string[],
115
+ parallelGroups: {} as { [groupName: string]: string[] }
116
+ };
117
+
118
+ for (const testFile of this.testfileArray) {
119
+ const filePath = testFile.path;
120
+ const fileName = plugins.path.basename(filePath);
121
+
122
+ // Check if file has parallel group pattern
123
+ const parallelMatch = fileName.match(/\.para__(\d+)\./);
124
+
125
+ if (parallelMatch) {
126
+ const groupNumber = parallelMatch[1];
127
+ const groupName = `para__${groupNumber}`;
128
+
129
+ if (!result.parallelGroups[groupName]) {
130
+ result.parallelGroups[groupName] = [];
131
+ }
132
+ result.parallelGroups[groupName].push(filePath);
133
+ } else {
134
+ // File runs serially
135
+ result.serial.push(filePath);
136
+ }
137
+ }
138
+
139
+ return result;
140
+ }
102
141
  }
@@ -15,6 +15,7 @@ export class TsTest {
15
15
  public testDir: TestDirectory;
16
16
  public executionMode: TestExecutionMode;
17
17
  public logger: TsTestLogger;
18
+ public filterTags: string[];
18
19
 
19
20
  public smartshellInstance = new plugins.smartshell.Smartshell({
20
21
  executor: 'bash',
@@ -25,53 +26,81 @@ export class TsTest {
25
26
 
26
27
  public tsbundleInstance = new plugins.tsbundle.TsBundle();
27
28
 
28
- constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
29
+ constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = []) {
29
30
  this.executionMode = executionModeArg;
30
31
  this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
31
32
  this.logger = new TsTestLogger(logOptions);
33
+ this.filterTags = tags;
32
34
  }
33
35
 
34
36
  async run() {
35
- const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
37
+ const testGroups = await this.testDir.getTestFileGroups();
38
+ const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
36
39
 
37
40
  // Log test discovery
38
41
  this.logger.testDiscovery(
39
- fileNamesToRun.length,
42
+ allFiles.length,
40
43
  this.testDir.testPath,
41
44
  this.executionMode
42
45
  );
43
46
 
44
47
  const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
45
48
  let fileIndex = 0;
46
- for (const fileNameArg of fileNamesToRun) {
49
+
50
+ // Execute serial tests first
51
+ for (const fileNameArg of testGroups.serial) {
47
52
  fileIndex++;
48
- switch (true) {
49
- case process.env.CI && fileNameArg.includes('.nonci.'):
50
- this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
51
- break;
52
- case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
53
- const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
54
- tapCombinator.addTapParser(tapParserBrowser);
55
- break;
56
- case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
57
- this.logger.sectionStart('Part 1: Chrome');
58
- const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
59
- tapCombinator.addTapParser(tapParserBothBrowser);
60
- this.logger.sectionEnd();
61
-
62
- this.logger.sectionStart('Part 2: Node');
63
- const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
64
- tapCombinator.addTapParser(tapParserBothNode);
65
- this.logger.sectionEnd();
66
- break;
67
- default:
68
- const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
69
- tapCombinator.addTapParser(tapParserNode);
70
- break;
53
+ await this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
54
+ }
55
+
56
+ // Execute parallel groups sequentially
57
+ const groupNames = Object.keys(testGroups.parallelGroups).sort();
58
+ for (const groupName of groupNames) {
59
+ const groupFiles = testGroups.parallelGroups[groupName];
60
+
61
+ if (groupFiles.length > 0) {
62
+ this.logger.sectionStart(`Parallel Group: ${groupName}`);
63
+
64
+ // Run all tests in this group in parallel
65
+ const parallelPromises = groupFiles.map(async (fileNameArg) => {
66
+ fileIndex++;
67
+ return this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
68
+ });
69
+
70
+ await Promise.all(parallelPromises);
71
+ this.logger.sectionEnd();
71
72
  }
72
73
  }
74
+
73
75
  tapCombinator.evaluate();
74
76
  }
77
+
78
+ private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
79
+ switch (true) {
80
+ case process.env.CI && fileNameArg.includes('.nonci.'):
81
+ this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
82
+ break;
83
+ case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
84
+ const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, totalFiles);
85
+ tapCombinator.addTapParser(tapParserBrowser);
86
+ break;
87
+ case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
88
+ this.logger.sectionStart('Part 1: Chrome');
89
+ const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, totalFiles);
90
+ tapCombinator.addTapParser(tapParserBothBrowser);
91
+ this.logger.sectionEnd();
92
+
93
+ this.logger.sectionStart('Part 2: Node');
94
+ const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
95
+ tapCombinator.addTapParser(tapParserBothNode);
96
+ this.logger.sectionEnd();
97
+ break;
98
+ default:
99
+ const tapParserNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
100
+ tapCombinator.addTapParser(tapParserNode);
101
+ break;
102
+ }
103
+ }
75
104
 
76
105
  public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> {
77
106
  this.logger.testFileStart(fileNameArg, 'node.js', index, total);
@@ -82,6 +111,11 @@ export class TsTest {
82
111
  if (process.argv.includes('--web')) {
83
112
  tsrunOptions += ' --web';
84
113
  }
114
+
115
+ // Set filter tags as environment variable
116
+ if (this.filterTags.length > 0) {
117
+ process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
118
+ }
85
119
 
86
120
  const execResultStreaming = await this.smartshellInstance.execStreamingSilent(
87
121
  `tsrun ${fileNameArg}${tsrunOptions}`