retold-data-service 2.0.16 → 2.0.18

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 (28) hide show
  1. package/.claude/launch.json +2 -2
  2. package/.quackage.json +19 -0
  3. package/package.json +13 -6
  4. package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
  5. package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
  6. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
  7. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
  8. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
  9. package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
  10. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
  11. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
  12. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
  13. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
  14. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
  15. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
  16. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
  17. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
  18. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
  19. package/source/services/data-cloner/web/data-cloner.js +7952 -0
  20. package/source/services/data-cloner/web/data-cloner.js.map +1 -0
  21. package/source/services/data-cloner/web/data-cloner.min.js +2 -0
  22. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
  23. package/source/services/data-cloner/web/index.html +17 -0
  24. package/test/DataCloner-Integration_tests.js +1205 -0
  25. package/test/DataCloner-Puppeteer_tests.js +502 -0
  26. package/test/integration-report.json +311 -0
  27. package/test/run-integration-tests.js +501 -0
  28. package/source/services/data-cloner/data-cloner-web.html +0 -2706
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Data Cloner Integration Test Runner
4
+ *
5
+ * Starts retold-harness and data-cloner servers, runs integration tests,
6
+ * collects timing data, and generates a summary report.
7
+ *
8
+ * Usage:
9
+ * node test/run-integration-tests.js [--skip-puppeteer] [--engines sqlite,mysql]
10
+ *
11
+ * @license MIT
12
+ * @author <steven@velozo.com>
13
+ */
14
+
15
+ const libChildProcess = require('child_process');
16
+ const libFs = require('fs');
17
+ const libPath = require('path');
18
+ const libOs = require('os');
19
+ const libHttp = require('http');
20
+
21
+ // ---- Configuration ----
22
+ const _HarnessPort = 9403;
23
+ const _ClonerPort = 9400;
24
+ const _HarnessBaseURL = `http://localhost:${_HarnessPort}`;
25
+ const _ClonerBaseURL = `http://localhost:${_ClonerPort}`;
26
+ const _RetoldRoot = libPath.resolve(__dirname, '..', '..', '..', '..');
27
+ const _HarnessDir = libPath.resolve(_RetoldRoot, 'modules', 'meadow', 'retold-harness');
28
+ const _ClonerDir = libPath.resolve(__dirname, '..');
29
+ const _HealthTimeout = 30000;
30
+ const _OverallTimeout = 600000;
31
+
32
+ // ---- Parse CLI args ----
33
+ let _SkipPuppeteer = process.argv.includes('--skip-puppeteer');
34
+ let _EngineArg = process.argv.find((a) => a.startsWith('--engines='));
35
+ let _RequestedEngines = _EngineArg ? _EngineArg.split('=')[1].split(',') : ['sqlite'];
36
+
37
+ // ---- State ----
38
+ let _HarnessProcess = null;
39
+ let _ClonerProcess = null;
40
+ let _TempDir = null;
41
+ let _Report = {
42
+ timestamp: new Date().toISOString(),
43
+ duration_ms: 0,
44
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
45
+ storage_engines: {},
46
+ suites: [],
47
+ puppeteer: { available: false, suites: [] }
48
+ };
49
+
50
+ // ---- Helpers ----
51
+
52
+ function fLog(pMessage)
53
+ {
54
+ let tmpTime = new Date().toISOString().substring(11, 19);
55
+ console.log(`[${tmpTime}] ${pMessage}`);
56
+ }
57
+
58
+ function fHttpGet(pURL, fCallback)
59
+ {
60
+ let tmpReq = libHttp.get(pURL,
61
+ (pRes) =>
62
+ {
63
+ let tmpChunks = [];
64
+ pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
65
+ pRes.on('end', () =>
66
+ {
67
+ try
68
+ {
69
+ let tmpData = JSON.parse(Buffer.concat(tmpChunks).toString());
70
+ return fCallback(null, tmpData);
71
+ }
72
+ catch (pParseError)
73
+ {
74
+ return fCallback(null, Buffer.concat(tmpChunks).toString());
75
+ }
76
+ });
77
+ });
78
+ tmpReq.on('error', (pError) => fCallback(pError));
79
+ tmpReq.setTimeout(5000, () => { tmpReq.destroy(); fCallback(new Error('timeout')); });
80
+ }
81
+
82
+ function fWaitForHealth(pURL, pLabel, fCallback)
83
+ {
84
+ let tmpStart = Date.now();
85
+
86
+ let fPoll = () =>
87
+ {
88
+ if (Date.now() - tmpStart > _HealthTimeout)
89
+ {
90
+ return fCallback(new Error(`${pLabel} did not become healthy within ${_HealthTimeout / 1000}s`));
91
+ }
92
+
93
+ fHttpGet(pURL,
94
+ (pError) =>
95
+ {
96
+ if (!pError)
97
+ {
98
+ return fCallback();
99
+ }
100
+ setTimeout(fPoll, 500);
101
+ });
102
+ };
103
+
104
+ setTimeout(fPoll, 1000);
105
+ }
106
+
107
+ function fKillProcess(pProcess, pLabel)
108
+ {
109
+ if (!pProcess || pProcess.exitCode !== null)
110
+ {
111
+ return;
112
+ }
113
+
114
+ fLog(`Stopping ${pLabel}...`);
115
+ pProcess.kill('SIGTERM');
116
+
117
+ setTimeout(() =>
118
+ {
119
+ if (pProcess.exitCode === null)
120
+ {
121
+ fLog(`Force-killing ${pLabel}...`);
122
+ pProcess.kill('SIGKILL');
123
+ }
124
+ }, 5000);
125
+ }
126
+
127
+ function fCleanup()
128
+ {
129
+ fKillProcess(_HarnessProcess, 'retold-harness');
130
+ fKillProcess(_ClonerProcess, 'data-cloner');
131
+
132
+ if (_TempDir && libFs.existsSync(_TempDir))
133
+ {
134
+ try
135
+ {
136
+ libFs.rmSync(_TempDir, { recursive: true, force: true });
137
+ fLog(`Cleaned up temp dir: ${_TempDir}`);
138
+ }
139
+ catch (pErr)
140
+ {
141
+ fLog(`Warning: could not clean temp dir: ${pErr.message}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ function fFormatDuration(pMs)
147
+ {
148
+ if (pMs < 1000) return `${pMs}ms`;
149
+ let tmpSec = (pMs / 1000).toFixed(1);
150
+ if (pMs < 60000) return `${tmpSec}s`;
151
+ let tmpMin = Math.floor(pMs / 60000);
152
+ tmpSec = ((pMs % 60000) / 1000).toFixed(0);
153
+ return `${tmpMin}m ${tmpSec}s`;
154
+ }
155
+
156
+ function fPrintReport()
157
+ {
158
+ console.log('\n' + '='.repeat(72));
159
+ console.log(' DATA CLONER INTEGRATION TEST REPORT');
160
+ console.log('='.repeat(72));
161
+ console.log(` Timestamp: ${_Report.timestamp}`);
162
+ console.log(` Duration: ${fFormatDuration(_Report.duration_ms)}`);
163
+ console.log(` Total: ${_Report.summary.total}`);
164
+ console.log(` Passed: ${_Report.summary.passed}`);
165
+ console.log(` Failed: ${_Report.summary.failed}`);
166
+ console.log(` Skipped: ${_Report.summary.skipped}`);
167
+ console.log('-'.repeat(72));
168
+
169
+ // Storage engines
170
+ console.log('\n Storage Engines:');
171
+ let tmpEngines = Object.keys(_Report.storage_engines);
172
+ for (let i = 0; i < tmpEngines.length; i++)
173
+ {
174
+ let tmpName = tmpEngines[i];
175
+ let tmpEngine = _Report.storage_engines[tmpName];
176
+ if (tmpEngine.status === 'pass')
177
+ {
178
+ console.log(` ${tmpName.padEnd(14)} PASS ${fFormatDuration(tmpEngine.sync_duration_ms).padStart(10)} ${tmpEngine.records_synced} records / ${tmpEngine.tables_synced} tables`);
179
+ }
180
+ else if (tmpEngine.status === 'fail')
181
+ {
182
+ console.log(` ${tmpName.padEnd(14)} FAIL ${tmpEngine.error || 'Unknown error'}`);
183
+ }
184
+ else
185
+ {
186
+ console.log(` ${tmpName.padEnd(14)} SKIP ${tmpEngine.reason || ''}`);
187
+ }
188
+ }
189
+
190
+ // Suites
191
+ console.log('\n Test Suites:');
192
+ for (let i = 0; i < _Report.suites.length; i++)
193
+ {
194
+ let tmpSuite = _Report.suites[i];
195
+ let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
196
+ let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
197
+ let tmpStatus = tmpFailCount > 0 ? 'FAIL' : 'PASS';
198
+ console.log(` ${tmpStatus === 'FAIL' ? 'X' : '+'} ${tmpSuite.name.padEnd(35)} ${tmpPassCount}/${tmpSuite.tests.length} passed ${fFormatDuration(tmpSuite.duration_ms)}`);
199
+
200
+ // Show failed tests
201
+ for (let j = 0; j < tmpSuite.tests.length; j++)
202
+ {
203
+ if (tmpSuite.tests[j].status === 'fail')
204
+ {
205
+ console.log(` FAIL: ${tmpSuite.tests[j].name}`);
206
+ if (tmpSuite.tests[j].error)
207
+ {
208
+ console.log(` ${tmpSuite.tests[j].error.substring(0, 120)}`);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // Puppeteer
215
+ if (_Report.puppeteer.available && _Report.puppeteer.suites.length > 0)
216
+ {
217
+ console.log('\n Puppeteer Tests:');
218
+ for (let i = 0; i < _Report.puppeteer.suites.length; i++)
219
+ {
220
+ let tmpSuite = _Report.puppeteer.suites[i];
221
+ let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
222
+ let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
223
+ let tmpStatus = tmpFailCount > 0 ? 'FAIL' : 'PASS';
224
+ console.log(` ${tmpStatus === 'FAIL' ? 'X' : '+'} ${tmpSuite.name.padEnd(35)} ${tmpPassCount}/${tmpSuite.tests.length} passed ${fFormatDuration(tmpSuite.duration_ms)}`);
225
+ }
226
+ }
227
+
228
+ console.log('\n' + '='.repeat(72) + '\n');
229
+ }
230
+
231
+ // ---- Main ----
232
+
233
+ let tmpOverallStart = Date.now();
234
+
235
+ // Create temp directory for SQLite
236
+ _TempDir = libPath.join(libOs.tmpdir(), `retold-cloner-test-${Date.now()}`);
237
+ libFs.mkdirSync(libPath.join(_TempDir, 'data'), { recursive: true });
238
+ fLog(`Temp directory: ${_TempDir}`);
239
+
240
+ // Handle cleanup on exit
241
+ process.on('SIGINT', () => { fCleanup(); process.exit(1); });
242
+ process.on('SIGTERM', () => { fCleanup(); process.exit(1); });
243
+ process.on('uncaughtException', (pErr) => { console.error('Uncaught:', pErr); fCleanup(); process.exit(1); });
244
+
245
+ // Step 1: Start retold-harness
246
+ fLog(`Starting retold-harness on port ${_HarnessPort}...`);
247
+ _HarnessProcess = libChildProcess.spawn('node',
248
+ ['source/Retold-Harness.js'],
249
+ {
250
+ cwd: _HarnessDir,
251
+ env: Object.assign({}, process.env, { PORT: String(_HarnessPort) }),
252
+ stdio: ['ignore', 'pipe', 'pipe']
253
+ });
254
+
255
+ _HarnessProcess.stdout.on('data', (pData) =>
256
+ {
257
+ let tmpLines = pData.toString().trim().split('\n');
258
+ for (let i = 0; i < tmpLines.length; i++)
259
+ {
260
+ if (tmpLines[i].indexOf('error') > -1 || tmpLines[i].indexOf('Error') > -1)
261
+ {
262
+ fLog(`[harness] ${tmpLines[i]}`);
263
+ }
264
+ }
265
+ });
266
+ _HarnessProcess.stderr.on('data', (pData) => fLog(`[harness stderr] ${pData.toString().trim()}`));
267
+ _HarnessProcess.on('exit', (pCode) => fLog(`retold-harness exited with code ${pCode}`));
268
+
269
+ // Step 2: Start data-cloner
270
+ fLog(`Starting data-cloner on port ${_ClonerPort}...`);
271
+ _ClonerProcess = libChildProcess.spawn('node',
272
+ [libPath.join(_ClonerDir, 'bin', 'retold-data-service-clone.js'), '--port', String(_ClonerPort)],
273
+ {
274
+ cwd: _TempDir,
275
+ env: Object.assign({}, process.env),
276
+ stdio: ['ignore', 'pipe', 'pipe']
277
+ });
278
+
279
+ _ClonerProcess.stdout.on('data', (pData) =>
280
+ {
281
+ let tmpLines = pData.toString().trim().split('\n');
282
+ for (let i = 0; i < tmpLines.length; i++)
283
+ {
284
+ if (tmpLines[i].indexOf('error') > -1 || tmpLines[i].indexOf('Error') > -1)
285
+ {
286
+ fLog(`[cloner] ${tmpLines[i]}`);
287
+ }
288
+ }
289
+ });
290
+ _ClonerProcess.stderr.on('data', (pData) => fLog(`[cloner stderr] ${pData.toString().trim()}`));
291
+ _ClonerProcess.on('exit', (pCode) => fLog(`data-cloner exited with code ${pCode}`));
292
+
293
+ // Step 3: Wait for both servers to be healthy
294
+ fLog('Waiting for servers to become healthy...');
295
+ fWaitForHealth(`${_HarnessBaseURL}/1.0/Books/Count`, 'retold-harness',
296
+ (pHarnessError) =>
297
+ {
298
+ if (pHarnessError)
299
+ {
300
+ console.error(pHarnessError.message);
301
+ fCleanup();
302
+ process.exit(1);
303
+ }
304
+ fLog('retold-harness is healthy.');
305
+
306
+ fWaitForHealth(`${_ClonerBaseURL}/clone/sync/status`, 'data-cloner',
307
+ (pClonerError) =>
308
+ {
309
+ if (pClonerError)
310
+ {
311
+ console.error(pClonerError.message);
312
+ fCleanup();
313
+ process.exit(1);
314
+ }
315
+ fLog('data-cloner is healthy.');
316
+ fLog('Both servers healthy. Running tests...\n');
317
+
318
+ // Step 4: Run Mocha programmatically
319
+ let Mocha = require('mocha');
320
+ let tmpMocha = new Mocha(
321
+ {
322
+ ui: 'tdd',
323
+ timeout: 120000,
324
+ slow: 5000,
325
+ reporter: 'spec'
326
+ });
327
+
328
+ // Set env vars for tests
329
+ process.env.CLONER_PORT = String(_ClonerPort);
330
+ process.env.HARNESS_PORT = String(_HarnessPort);
331
+ process.env.CLONER_TEMP_DIR = _TempDir;
332
+ process.env.REQUESTED_ENGINES = _RequestedEngines.join(',');
333
+ process.env.HARNESS_SCHEMA_PATH = libPath.join(_HarnessDir, 'source', 'schemas', 'bookstore', 'MeadowModel-Extended.json');
334
+
335
+ tmpMocha.addFile(libPath.join(__dirname, 'DataCloner-Integration_tests.js'));
336
+
337
+ // Collect results
338
+ let tmpSuiteMap = {};
339
+ let tmpCurrentSuite = null;
340
+
341
+ let tmpRunner = tmpMocha.run(
342
+ (pFailures) =>
343
+ {
344
+ // Finalize suite data
345
+ let tmpSuiteNames = Object.keys(tmpSuiteMap);
346
+ for (let i = 0; i < tmpSuiteNames.length; i++)
347
+ {
348
+ let tmpS = tmpSuiteMap[tmpSuiteNames[i]];
349
+ tmpS.duration_ms = tmpS.tests.reduce((a, t) => a + t.duration_ms, 0);
350
+ _Report.suites.push(tmpS);
351
+ }
352
+
353
+ _Report.summary.total = tmpRunner.stats.tests;
354
+ _Report.summary.passed = tmpRunner.stats.passes;
355
+ _Report.summary.failed = tmpRunner.stats.failures;
356
+ _Report.summary.skipped = tmpRunner.stats.pending || 0;
357
+
358
+ // Step 5: Optionally run Puppeteer tests
359
+ let fFinish = () =>
360
+ {
361
+ _Report.duration_ms = Date.now() - tmpOverallStart;
362
+
363
+ // Write report
364
+ let tmpReportPath = libPath.join(__dirname, 'integration-report.json');
365
+ try
366
+ {
367
+ libFs.writeFileSync(tmpReportPath, JSON.stringify(_Report, null, '\t'), 'utf8');
368
+ fLog(`Report written to ${tmpReportPath}`);
369
+ }
370
+ catch (pWriteErr)
371
+ {
372
+ fLog(`Warning: could not write report: ${pWriteErr.message}`);
373
+ }
374
+
375
+ fPrintReport();
376
+ fCleanup();
377
+ process.exit(pFailures > 0 ? 1 : 0);
378
+ };
379
+
380
+ if (!_SkipPuppeteer)
381
+ {
382
+ try
383
+ {
384
+ require.resolve('puppeteer');
385
+ _Report.puppeteer.available = true;
386
+ fLog('\nRunning Puppeteer tests...');
387
+
388
+ let tmpPuppeteerMocha = new Mocha(
389
+ {
390
+ ui: 'tdd',
391
+ timeout: 60000,
392
+ slow: 10000,
393
+ reporter: 'spec'
394
+ });
395
+
396
+ let tmpPuppeteerTestPath = libPath.join(__dirname, 'DataCloner-Puppeteer_tests.js');
397
+ if (libFs.existsSync(tmpPuppeteerTestPath))
398
+ {
399
+ tmpPuppeteerMocha.addFile(tmpPuppeteerTestPath);
400
+
401
+ let tmpPuppeteerSuiteMap = {};
402
+ let tmpPRunner = tmpPuppeteerMocha.run(
403
+ (pPuppeteerFailures) =>
404
+ {
405
+ let tmpPSuiteNames = Object.keys(tmpPuppeteerSuiteMap);
406
+ for (let i = 0; i < tmpPSuiteNames.length; i++)
407
+ {
408
+ let tmpPS = tmpPuppeteerSuiteMap[tmpPSuiteNames[i]];
409
+ tmpPS.duration_ms = tmpPS.tests.reduce((a, t) => a + t.duration_ms, 0);
410
+ _Report.puppeteer.suites.push(tmpPS);
411
+ }
412
+ _Report.summary.total += tmpPRunner.stats.tests;
413
+ _Report.summary.passed += tmpPRunner.stats.passes;
414
+ _Report.summary.failed += tmpPRunner.stats.failures;
415
+ fFinish();
416
+ });
417
+
418
+ tmpPRunner.on('suite', (pSuite) =>
419
+ {
420
+ if (pSuite.title && pSuite.title !== '')
421
+ {
422
+ tmpPuppeteerSuiteMap[pSuite.title] = { name: pSuite.title, tests: [], duration_ms: 0 };
423
+ }
424
+ });
425
+ tmpPRunner.on('pass', (pTest) =>
426
+ {
427
+ let tmpSuiteName = pTest.parent ? pTest.parent.title : 'Unknown';
428
+ if (tmpPuppeteerSuiteMap[tmpSuiteName])
429
+ {
430
+ tmpPuppeteerSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'pass', duration_ms: pTest.duration || 0 });
431
+ }
432
+ });
433
+ tmpPRunner.on('fail', (pTest, pErr) =>
434
+ {
435
+ let tmpSuiteName = pTest.parent ? pTest.parent.title : 'Unknown';
436
+ if (tmpPuppeteerSuiteMap[tmpSuiteName])
437
+ {
438
+ tmpPuppeteerSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'fail', duration_ms: pTest.duration || 0, error: pErr.message });
439
+ }
440
+ });
441
+ }
442
+ else
443
+ {
444
+ fLog('Puppeteer test file not found, skipping.');
445
+ fFinish();
446
+ }
447
+ }
448
+ catch (pResolveErr)
449
+ {
450
+ fLog('Puppeteer not installed, skipping browser tests. Install with: npm install --save-dev puppeteer');
451
+ fFinish();
452
+ }
453
+ }
454
+ else
455
+ {
456
+ fLog('Skipping Puppeteer tests (--skip-puppeteer).');
457
+ fFinish();
458
+ }
459
+ });
460
+
461
+ // Collect per-test timing
462
+ tmpRunner.on('suite', (pSuite) =>
463
+ {
464
+ if (pSuite.title && pSuite.title !== '')
465
+ {
466
+ tmpCurrentSuite = pSuite.title;
467
+ if (!tmpSuiteMap[tmpCurrentSuite])
468
+ {
469
+ tmpSuiteMap[tmpCurrentSuite] = { name: tmpCurrentSuite, tests: [], duration_ms: 0 };
470
+ }
471
+ }
472
+ });
473
+ tmpRunner.on('pass', (pTest) =>
474
+ {
475
+ let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
476
+ if (!tmpSuiteMap[tmpSuiteName])
477
+ {
478
+ tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
479
+ }
480
+ tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'pass', duration_ms: pTest.duration || 0 });
481
+ });
482
+ tmpRunner.on('fail', (pTest, pErr) =>
483
+ {
484
+ let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
485
+ if (!tmpSuiteMap[tmpSuiteName])
486
+ {
487
+ tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
488
+ }
489
+ tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'fail', duration_ms: pTest.duration || 0, error: pErr.message });
490
+ });
491
+ tmpRunner.on('pending', (pTest) =>
492
+ {
493
+ let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
494
+ if (!tmpSuiteMap[tmpSuiteName])
495
+ {
496
+ tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
497
+ }
498
+ tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'skip', duration_ms: 0 });
499
+ });
500
+ });
501
+ });