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.
- package/.claude/launch.json +2 -2
- package/.quackage.json +19 -0
- package/package.json +13 -6
- package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
- package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
- package/source/services/data-cloner/web/data-cloner.js +7952 -0
- package/source/services/data-cloner/web/data-cloner.js.map +1 -0
- package/source/services/data-cloner/web/data-cloner.min.js +2 -0
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
- package/source/services/data-cloner/web/index.html +17 -0
- package/test/DataCloner-Integration_tests.js +1205 -0
- package/test/DataCloner-Puppeteer_tests.js +502 -0
- package/test/integration-report.json +311 -0
- package/test/run-integration-tests.js +501 -0
- package/source/services/data-cloner/data-cloner-web.html +0 -2706
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Cloner Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Exercises the full data-cloner pipeline against a live retold-harness server.
|
|
5
|
+
* Run via: node test/run-integration-tests.js
|
|
6
|
+
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @author <steven@velozo.com>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var Chai = require('chai');
|
|
12
|
+
var Expect = Chai.expect;
|
|
13
|
+
|
|
14
|
+
var libHttp = require('http');
|
|
15
|
+
var libFs = require('fs');
|
|
16
|
+
|
|
17
|
+
// Ports set by run-integration-tests.js
|
|
18
|
+
var _ClonerPort = parseInt(process.env.CLONER_PORT, 10) || 9400;
|
|
19
|
+
var _HarnessPort = parseInt(process.env.HARNESS_PORT, 10) || 9403;
|
|
20
|
+
var _ClonerBase = `http://localhost:${_ClonerPort}`;
|
|
21
|
+
var _HarnessBase = `http://localhost:${_HarnessPort}`;
|
|
22
|
+
var _RequestedEngines = (process.env.REQUESTED_ENGINES || 'sqlite').split(',');
|
|
23
|
+
|
|
24
|
+
// Load the harness schema from disk (retold-harness doesn't serve /Retold/Models)
|
|
25
|
+
var _HarnessSchemaPath = process.env.HARNESS_SCHEMA_PATH || require('path').resolve(__dirname, '..', '..', 'retold-harness', 'source', 'schemas', 'bookstore', 'MeadowModel-Extended.json');
|
|
26
|
+
var _HarnessSchema = JSON.parse(libFs.readFileSync(_HarnessSchemaPath, 'utf8'));
|
|
27
|
+
|
|
28
|
+
// Track storage engine results for the report
|
|
29
|
+
var _EngineResults = {};
|
|
30
|
+
|
|
31
|
+
// ---- HTTP Helpers (same pattern as DataCloner-Command-Headless.js) ----
|
|
32
|
+
|
|
33
|
+
// Parse JSON, handling potential double-encoding from restify
|
|
34
|
+
var fParseJSON = function(pRaw)
|
|
35
|
+
{
|
|
36
|
+
var tmpData = JSON.parse(pRaw);
|
|
37
|
+
// Handle double-encoded JSON (restify sometimes wraps strings)
|
|
38
|
+
if (typeof tmpData === 'string')
|
|
39
|
+
{
|
|
40
|
+
try { tmpData = JSON.parse(tmpData); }
|
|
41
|
+
catch (e) { /* leave as string */ }
|
|
42
|
+
}
|
|
43
|
+
return tmpData;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
var fParseResponse = function(pChunks)
|
|
47
|
+
{
|
|
48
|
+
var tmpRaw = Buffer.concat(pChunks).toString();
|
|
49
|
+
try
|
|
50
|
+
{
|
|
51
|
+
return fParseJSON(tmpRaw);
|
|
52
|
+
}
|
|
53
|
+
catch (pParseError)
|
|
54
|
+
{
|
|
55
|
+
return tmpRaw;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
var fPost = function(pPath, pBody, fCallback)
|
|
60
|
+
{
|
|
61
|
+
var tmpPayload = JSON.stringify(pBody);
|
|
62
|
+
var tmpURL = new URL(_ClonerBase + pPath);
|
|
63
|
+
var tmpOpts =
|
|
64
|
+
{
|
|
65
|
+
hostname: tmpURL.hostname,
|
|
66
|
+
port: tmpURL.port,
|
|
67
|
+
path: tmpURL.pathname,
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers:
|
|
70
|
+
{
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Content-Length': Buffer.byteLength(tmpPayload)
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
var tmpReq = libHttp.request(tmpOpts,
|
|
77
|
+
(pRes) =>
|
|
78
|
+
{
|
|
79
|
+
var tmpChunks = [];
|
|
80
|
+
pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
|
|
81
|
+
pRes.on('end', () =>
|
|
82
|
+
{
|
|
83
|
+
fCallback(null, fParseResponse(tmpChunks));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
tmpReq.on('error', fCallback);
|
|
87
|
+
tmpReq.write(tmpPayload);
|
|
88
|
+
tmpReq.end();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
var fGet = function(pPath, fCallback)
|
|
92
|
+
{
|
|
93
|
+
libHttp.get(_ClonerBase + pPath,
|
|
94
|
+
(pRes) =>
|
|
95
|
+
{
|
|
96
|
+
var tmpChunks = [];
|
|
97
|
+
pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
|
|
98
|
+
pRes.on('end', () =>
|
|
99
|
+
{
|
|
100
|
+
fCallback(null, fParseResponse(tmpChunks));
|
|
101
|
+
});
|
|
102
|
+
}).on('error', fCallback);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
var fGetHarness = function(pPath, fCallback)
|
|
106
|
+
{
|
|
107
|
+
libHttp.get(_HarnessBase + pPath,
|
|
108
|
+
(pRes) =>
|
|
109
|
+
{
|
|
110
|
+
var tmpChunks = [];
|
|
111
|
+
pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
|
|
112
|
+
pRes.on('end', () =>
|
|
113
|
+
{
|
|
114
|
+
fCallback(null, fParseResponse(tmpChunks));
|
|
115
|
+
});
|
|
116
|
+
}).on('error', fCallback);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
var fWaitForSyncComplete = function(fCallback, pTimeout)
|
|
120
|
+
{
|
|
121
|
+
var tmpTimeout = pTimeout || 120000;
|
|
122
|
+
var tmpStart = Date.now();
|
|
123
|
+
|
|
124
|
+
var fPoll = function()
|
|
125
|
+
{
|
|
126
|
+
fGet('/clone/sync/status',
|
|
127
|
+
(pError, pStatus) =>
|
|
128
|
+
{
|
|
129
|
+
if (pError)
|
|
130
|
+
{
|
|
131
|
+
if (Date.now() - tmpStart > tmpTimeout) return fCallback(new Error('Sync timeout (poll error)'));
|
|
132
|
+
return setTimeout(fPoll, 2000);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pStatus && !pStatus.Running)
|
|
136
|
+
{
|
|
137
|
+
return fCallback(null, pStatus);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (Date.now() - tmpStart > tmpTimeout)
|
|
141
|
+
{
|
|
142
|
+
return fCallback(new Error('Sync timeout'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setTimeout(fPoll, 2000);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
setTimeout(fPoll, 1000);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Full pipeline: reset → configure connection → session → schema → deploy → sync → report
|
|
153
|
+
var fRunFullPipeline = function(pProviderConfig, pMaxRecords, fCallback)
|
|
154
|
+
{
|
|
155
|
+
var tmpSyncStart;
|
|
156
|
+
|
|
157
|
+
// Step 1: Reset
|
|
158
|
+
fPost('/clone/reset', {},
|
|
159
|
+
(pResetErr) =>
|
|
160
|
+
{
|
|
161
|
+
// Step 2: Configure connection (skip for default SQLite)
|
|
162
|
+
var fAfterConnection = function()
|
|
163
|
+
{
|
|
164
|
+
// Step 3: Configure session
|
|
165
|
+
fPost('/clone/session/configure', { ServerURL: `${_HarnessBase}/1.0/` },
|
|
166
|
+
(pSessionErr, pSessionData) =>
|
|
167
|
+
{
|
|
168
|
+
if (pSessionErr || !pSessionData || !pSessionData.Success)
|
|
169
|
+
{
|
|
170
|
+
return fCallback(new Error('Session configure failed: ' + (pSessionErr || JSON.stringify(pSessionData))));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 4: Fetch schema
|
|
174
|
+
fPost('/clone/schema/fetch', { Schema: _HarnessSchema },
|
|
175
|
+
(pSchemaErr, pSchemaData) =>
|
|
176
|
+
{
|
|
177
|
+
if (pSchemaErr || !pSchemaData || !pSchemaData.Success)
|
|
178
|
+
{
|
|
179
|
+
return fCallback(new Error('Schema fetch failed: ' + (pSchemaErr || JSON.stringify(pSchemaData))));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Step 5: Deploy
|
|
183
|
+
fPost('/clone/schema/deploy', { Tables: [] },
|
|
184
|
+
(pDeployErr, pDeployData) =>
|
|
185
|
+
{
|
|
186
|
+
if (pDeployErr || !pDeployData || !pDeployData.Success)
|
|
187
|
+
{
|
|
188
|
+
return fCallback(new Error('Deploy failed: ' + (pDeployErr || JSON.stringify(pDeployData))));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 6: Sync
|
|
192
|
+
tmpSyncStart = Date.now();
|
|
193
|
+
fPost('/clone/sync/start', { SyncMode: 'Initial', MaxRecordsPerEntity: pMaxRecords || 50 },
|
|
194
|
+
(pSyncErr, pSyncData) =>
|
|
195
|
+
{
|
|
196
|
+
if (pSyncErr || !pSyncData || !pSyncData.Success)
|
|
197
|
+
{
|
|
198
|
+
return fCallback(new Error('Sync start failed: ' + (pSyncErr || JSON.stringify(pSyncData))));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Step 7: Wait for completion
|
|
202
|
+
fWaitForSyncComplete(
|
|
203
|
+
(pWaitErr, pFinalStatus) =>
|
|
204
|
+
{
|
|
205
|
+
if (pWaitErr)
|
|
206
|
+
{
|
|
207
|
+
return fCallback(pWaitErr);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var tmpSyncDuration = Date.now() - tmpSyncStart;
|
|
211
|
+
|
|
212
|
+
// Step 8: Get report
|
|
213
|
+
fGet('/clone/sync/report',
|
|
214
|
+
(pReportErr, pReport) =>
|
|
215
|
+
{
|
|
216
|
+
return fCallback(null,
|
|
217
|
+
{
|
|
218
|
+
syncDuration: tmpSyncDuration,
|
|
219
|
+
finalStatus: pFinalStatus,
|
|
220
|
+
report: pReport,
|
|
221
|
+
schemaData: pSchemaData,
|
|
222
|
+
deployData: pDeployData
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (pProviderConfig && pProviderConfig.Provider !== 'SQLite')
|
|
233
|
+
{
|
|
234
|
+
fPost('/clone/connection/configure', pProviderConfig,
|
|
235
|
+
(pConnErr, pConnData) =>
|
|
236
|
+
{
|
|
237
|
+
if (pConnErr || !pConnData || !pConnData.Success)
|
|
238
|
+
{
|
|
239
|
+
return fCallback(new Error('Connection configure failed: ' + (pConnErr || JSON.stringify(pConnData))));
|
|
240
|
+
}
|
|
241
|
+
fAfterConnection();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
{
|
|
246
|
+
fAfterConnection();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// ---- Harness data cache (fetched once) ----
|
|
252
|
+
var _HarnessBookCount = 0;
|
|
253
|
+
var _HarnessAuthorCount = 0;
|
|
254
|
+
var _HarnessBookRecord1 = null;
|
|
255
|
+
|
|
256
|
+
// ================================================================
|
|
257
|
+
// TEST SUITES
|
|
258
|
+
// ================================================================
|
|
259
|
+
|
|
260
|
+
suite
|
|
261
|
+
(
|
|
262
|
+
'Data Cloner Integration',
|
|
263
|
+
function()
|
|
264
|
+
{
|
|
265
|
+
this.timeout(120000);
|
|
266
|
+
|
|
267
|
+
// ---- Connection Management ----
|
|
268
|
+
suite
|
|
269
|
+
(
|
|
270
|
+
'Connection Management',
|
|
271
|
+
function()
|
|
272
|
+
{
|
|
273
|
+
test
|
|
274
|
+
(
|
|
275
|
+
'Should show initial connection status',
|
|
276
|
+
function(fDone)
|
|
277
|
+
{
|
|
278
|
+
fGet('/clone/connection/status',
|
|
279
|
+
(pError, pData) =>
|
|
280
|
+
{
|
|
281
|
+
Expect(pError).to.equal(null);
|
|
282
|
+
Expect(pData).to.be.an('object');
|
|
283
|
+
Expect(pData.Provider).to.equal('SQLite');
|
|
284
|
+
// Initially may or may not be connected depending on startup
|
|
285
|
+
fDone();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
test
|
|
290
|
+
(
|
|
291
|
+
'Should connect SQLite via configure',
|
|
292
|
+
function(fDone)
|
|
293
|
+
{
|
|
294
|
+
this.timeout(10000);
|
|
295
|
+
fPost('/clone/connection/configure',
|
|
296
|
+
{
|
|
297
|
+
Provider: 'SQLite',
|
|
298
|
+
Config: {}
|
|
299
|
+
},
|
|
300
|
+
(pError, pData) =>
|
|
301
|
+
{
|
|
302
|
+
Expect(pError).to.equal(null);
|
|
303
|
+
Expect(pData).to.be.an('object');
|
|
304
|
+
Expect(pData.Success).to.equal(true);
|
|
305
|
+
fDone();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
test
|
|
310
|
+
(
|
|
311
|
+
'Should show connected after configure',
|
|
312
|
+
function(fDone)
|
|
313
|
+
{
|
|
314
|
+
fGet('/clone/connection/status',
|
|
315
|
+
(pError, pData) =>
|
|
316
|
+
{
|
|
317
|
+
Expect(pError).to.equal(null);
|
|
318
|
+
Expect(pData).to.be.an('object');
|
|
319
|
+
Expect(pData.Provider).to.equal('SQLite');
|
|
320
|
+
Expect(pData.Connected).to.equal(true);
|
|
321
|
+
fDone();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// ---- Session Configuration ----
|
|
329
|
+
suite
|
|
330
|
+
(
|
|
331
|
+
'Session Configuration',
|
|
332
|
+
function()
|
|
333
|
+
{
|
|
334
|
+
test
|
|
335
|
+
(
|
|
336
|
+
'Should configure session with retold-harness URL',
|
|
337
|
+
function(fDone)
|
|
338
|
+
{
|
|
339
|
+
fPost('/clone/session/configure',
|
|
340
|
+
{ ServerURL: `${_HarnessBase}/1.0/` },
|
|
341
|
+
(pError, pData) =>
|
|
342
|
+
{
|
|
343
|
+
Expect(pError).to.equal(null);
|
|
344
|
+
Expect(pData.Success).to.equal(true);
|
|
345
|
+
Expect(pData.ServerURL).to.contain(String(_HarnessPort));
|
|
346
|
+
fDone();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
test
|
|
351
|
+
(
|
|
352
|
+
'Should show session as configured',
|
|
353
|
+
function(fDone)
|
|
354
|
+
{
|
|
355
|
+
fGet('/clone/session/check',
|
|
356
|
+
(pError, pData) =>
|
|
357
|
+
{
|
|
358
|
+
Expect(pError).to.equal(null);
|
|
359
|
+
Expect(pData.Configured).to.equal(true);
|
|
360
|
+
Expect(pData.ServerURL).to.contain(String(_HarnessPort));
|
|
361
|
+
fDone();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
test
|
|
366
|
+
(
|
|
367
|
+
'Should reject session configure without ServerURL',
|
|
368
|
+
function(fDone)
|
|
369
|
+
{
|
|
370
|
+
fPost('/clone/session/configure', {},
|
|
371
|
+
(pError, pData) =>
|
|
372
|
+
{
|
|
373
|
+
Expect(pData.Success).to.equal(false);
|
|
374
|
+
Expect(pData.Error).to.contain('ServerURL');
|
|
375
|
+
fDone();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// ---- Schema Fetch ----
|
|
383
|
+
suite
|
|
384
|
+
(
|
|
385
|
+
'Schema Fetch',
|
|
386
|
+
function()
|
|
387
|
+
{
|
|
388
|
+
test
|
|
389
|
+
(
|
|
390
|
+
'Should reconfigure session for fresh schema fetch',
|
|
391
|
+
function(fDone)
|
|
392
|
+
{
|
|
393
|
+
fPost('/clone/session/configure',
|
|
394
|
+
{ ServerURL: `${_HarnessBase}/1.0/` },
|
|
395
|
+
(pError, pData) =>
|
|
396
|
+
{
|
|
397
|
+
Expect(pData.Success).to.equal(true);
|
|
398
|
+
fDone();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
test
|
|
403
|
+
(
|
|
404
|
+
'Should fetch schema from retold-harness',
|
|
405
|
+
function(fDone)
|
|
406
|
+
{
|
|
407
|
+
fPost('/clone/schema/fetch', { Schema: _HarnessSchema },
|
|
408
|
+
(pError, pData) =>
|
|
409
|
+
{
|
|
410
|
+
Expect(pError).to.equal(null);
|
|
411
|
+
Expect(pData.Success).to.equal(true);
|
|
412
|
+
Expect(pData.TableCount).to.be.greaterThan(0);
|
|
413
|
+
Expect(pData.Tables).to.be.an('array');
|
|
414
|
+
// Bookstore schema has 8 entities
|
|
415
|
+
Expect(pData.Tables).to.include('Book');
|
|
416
|
+
Expect(pData.Tables).to.include('Author');
|
|
417
|
+
Expect(pData.Tables).to.include('User');
|
|
418
|
+
Expect(pData.Tables).to.include('BookAuthorJoin');
|
|
419
|
+
Expect(pData.Tables).to.include('BookPrice');
|
|
420
|
+
Expect(pData.Tables).to.include('BookStore');
|
|
421
|
+
Expect(pData.Tables).to.include('BookStoreInventory');
|
|
422
|
+
Expect(pData.Tables).to.include('Review');
|
|
423
|
+
fDone();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// ---- Schema Deploy ----
|
|
431
|
+
suite
|
|
432
|
+
(
|
|
433
|
+
'Schema Deploy',
|
|
434
|
+
function()
|
|
435
|
+
{
|
|
436
|
+
test
|
|
437
|
+
(
|
|
438
|
+
'Should deploy all tables',
|
|
439
|
+
function(fDone)
|
|
440
|
+
{
|
|
441
|
+
this.timeout(30000);
|
|
442
|
+
|
|
443
|
+
fPost('/clone/schema/deploy', { Tables: [] },
|
|
444
|
+
(pError, pData) =>
|
|
445
|
+
{
|
|
446
|
+
Expect(pError).to.equal(null);
|
|
447
|
+
Expect(pData.Success).to.equal(true);
|
|
448
|
+
Expect(pData.SyncEntities).to.be.an('array');
|
|
449
|
+
Expect(pData.SyncEntities.length).to.be.greaterThan(0);
|
|
450
|
+
Expect(pData.SyncEntities).to.include('Book');
|
|
451
|
+
Expect(pData.SyncEntities).to.include('Author');
|
|
452
|
+
fDone();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// ---- Initial Sync (SQLite) ----
|
|
460
|
+
suite
|
|
461
|
+
(
|
|
462
|
+
'Initial Sync (SQLite)',
|
|
463
|
+
function()
|
|
464
|
+
{
|
|
465
|
+
this.timeout(120000);
|
|
466
|
+
|
|
467
|
+
var _SyncStatus = null;
|
|
468
|
+
var _SyncReport = null;
|
|
469
|
+
var _SyncDuration = 0;
|
|
470
|
+
|
|
471
|
+
test
|
|
472
|
+
(
|
|
473
|
+
'Should start initial sync with record cap',
|
|
474
|
+
function(fDone)
|
|
475
|
+
{
|
|
476
|
+
var tmpStart = Date.now();
|
|
477
|
+
|
|
478
|
+
fPost('/clone/sync/start',
|
|
479
|
+
{
|
|
480
|
+
SyncMode: 'Initial',
|
|
481
|
+
MaxRecordsPerEntity: 50
|
|
482
|
+
},
|
|
483
|
+
(pError, pData) =>
|
|
484
|
+
{
|
|
485
|
+
Expect(pError).to.equal(null);
|
|
486
|
+
Expect(pData.Success).to.equal(true);
|
|
487
|
+
|
|
488
|
+
// Wait for sync to complete
|
|
489
|
+
fWaitForSyncComplete(
|
|
490
|
+
(pWaitErr, pStatus) =>
|
|
491
|
+
{
|
|
492
|
+
Expect(pWaitErr).to.equal(null);
|
|
493
|
+
_SyncStatus = pStatus;
|
|
494
|
+
_SyncDuration = Date.now() - tmpStart;
|
|
495
|
+
fDone();
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
test
|
|
501
|
+
(
|
|
502
|
+
'All tables should have completed',
|
|
503
|
+
function()
|
|
504
|
+
{
|
|
505
|
+
Expect(_SyncStatus).to.be.an('object');
|
|
506
|
+
Expect(_SyncStatus.Running).to.equal(false);
|
|
507
|
+
|
|
508
|
+
var tmpTableNames = Object.keys(_SyncStatus.Tables);
|
|
509
|
+
Expect(tmpTableNames.length).to.be.greaterThan(0);
|
|
510
|
+
|
|
511
|
+
for (var i = 0; i < tmpTableNames.length; i++)
|
|
512
|
+
{
|
|
513
|
+
var tmpTable = _SyncStatus.Tables[tmpTableNames[i]];
|
|
514
|
+
Expect(['Complete', 'Partial']).to.include(tmpTable.Status,
|
|
515
|
+
`Table ${tmpTableNames[i]} has unexpected status: ${tmpTable.Status}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
test
|
|
520
|
+
(
|
|
521
|
+
'Should have a valid sync report',
|
|
522
|
+
function(fDone)
|
|
523
|
+
{
|
|
524
|
+
fGet('/clone/sync/report',
|
|
525
|
+
(pError, pData) =>
|
|
526
|
+
{
|
|
527
|
+
Expect(pError).to.equal(null);
|
|
528
|
+
Expect(pData).to.be.an('object');
|
|
529
|
+
Expect(pData.ReportVersion).to.be.a('string');
|
|
530
|
+
Expect(pData.RunID).to.be.a('string');
|
|
531
|
+
Expect(['Success', 'Partial']).to.include(pData.Outcome);
|
|
532
|
+
Expect(pData.Summary).to.be.an('object');
|
|
533
|
+
Expect(pData.Summary.TotalTables).to.be.greaterThan(0);
|
|
534
|
+
Expect(pData.Tables).to.be.an('array');
|
|
535
|
+
Expect(pData.Tables.length).to.equal(pData.Summary.TotalTables);
|
|
536
|
+
Expect(pData.RunTimestamps).to.be.an('object');
|
|
537
|
+
Expect(pData.RunTimestamps.Start).to.be.a('string');
|
|
538
|
+
Expect(pData.RunTimestamps.End).to.be.a('string');
|
|
539
|
+
Expect(pData.RunTimestamps.DurationSeconds).to.be.at.least(0);
|
|
540
|
+
|
|
541
|
+
_SyncReport = pData;
|
|
542
|
+
|
|
543
|
+
// Record engine result
|
|
544
|
+
_EngineResults['SQLite'] =
|
|
545
|
+
{
|
|
546
|
+
status: 'pass',
|
|
547
|
+
sync_duration_ms: _SyncDuration,
|
|
548
|
+
records_synced: pData.Summary.TotalSynced || 0,
|
|
549
|
+
tables_synced: pData.Summary.TotalTables || 0
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
fDone();
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
test
|
|
557
|
+
(
|
|
558
|
+
'Report tables should have timing data',
|
|
559
|
+
function()
|
|
560
|
+
{
|
|
561
|
+
Expect(_SyncReport).to.be.an('object');
|
|
562
|
+
|
|
563
|
+
for (var i = 0; i < _SyncReport.Tables.length; i++)
|
|
564
|
+
{
|
|
565
|
+
var tmpTable = _SyncReport.Tables[i];
|
|
566
|
+
Expect(tmpTable.Name).to.be.a('string');
|
|
567
|
+
Expect(['Complete', 'Partial', 'Error']).to.include(tmpTable.Status);
|
|
568
|
+
Expect(tmpTable).to.have.property('DurationSeconds');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// ---- Data Integrity (run right after initial sync, before pre-count resets) ----
|
|
576
|
+
suite
|
|
577
|
+
(
|
|
578
|
+
'Data Integrity',
|
|
579
|
+
function()
|
|
580
|
+
{
|
|
581
|
+
this.timeout(30000);
|
|
582
|
+
|
|
583
|
+
test
|
|
584
|
+
(
|
|
585
|
+
'Should fetch harness reference data',
|
|
586
|
+
function(fDone)
|
|
587
|
+
{
|
|
588
|
+
fGetHarness('/1.0/Books/Count',
|
|
589
|
+
(pError, pData) =>
|
|
590
|
+
{
|
|
591
|
+
Expect(pError).to.equal(null);
|
|
592
|
+
Expect(pData.Count).to.be.greaterThan(0);
|
|
593
|
+
_HarnessBookCount = pData.Count;
|
|
594
|
+
|
|
595
|
+
fGetHarness('/1.0/Authors/Count',
|
|
596
|
+
(pErr2, pData2) =>
|
|
597
|
+
{
|
|
598
|
+
Expect(pData2.Count).to.be.greaterThan(0);
|
|
599
|
+
_HarnessAuthorCount = pData2.Count;
|
|
600
|
+
|
|
601
|
+
fGetHarness('/1.0/Book/1',
|
|
602
|
+
(pErr3, pData3) =>
|
|
603
|
+
{
|
|
604
|
+
Expect(pData3.IDBook).to.equal(1);
|
|
605
|
+
_HarnessBookRecord1 = pData3;
|
|
606
|
+
fDone();
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
test
|
|
613
|
+
(
|
|
614
|
+
'Local book count should match sync (capped)',
|
|
615
|
+
function(fDone)
|
|
616
|
+
{
|
|
617
|
+
fGet('/1.0/Books/Count',
|
|
618
|
+
(pError, pData) =>
|
|
619
|
+
{
|
|
620
|
+
Expect(pError).to.equal(null);
|
|
621
|
+
Expect(pData.Count).to.be.greaterThan(0);
|
|
622
|
+
// Local count should be <= remote count (capped sync means fewer or equal)
|
|
623
|
+
Expect(pData.Count).to.be.at.most(_HarnessBookCount);
|
|
624
|
+
fDone();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
test
|
|
629
|
+
(
|
|
630
|
+
'Local Book 1 should match harness data',
|
|
631
|
+
function(fDone)
|
|
632
|
+
{
|
|
633
|
+
fGet('/1.0/Book/1',
|
|
634
|
+
(pError, pData) =>
|
|
635
|
+
{
|
|
636
|
+
Expect(pError).to.equal(null);
|
|
637
|
+
Expect(pData.IDBook).to.equal(1);
|
|
638
|
+
Expect(pData.Title).to.equal(_HarnessBookRecord1.Title);
|
|
639
|
+
Expect(pData.Genre).to.equal(_HarnessBookRecord1.Genre);
|
|
640
|
+
fDone();
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
test
|
|
645
|
+
(
|
|
646
|
+
'Local author count should match sync (capped)',
|
|
647
|
+
function(fDone)
|
|
648
|
+
{
|
|
649
|
+
fGet('/1.0/Authors/Count',
|
|
650
|
+
(pError, pData) =>
|
|
651
|
+
{
|
|
652
|
+
Expect(pError).to.equal(null);
|
|
653
|
+
Expect(pData.Count).to.be.greaterThan(0);
|
|
654
|
+
Expect(pData.Count).to.be.at.most(_HarnessAuthorCount);
|
|
655
|
+
fDone();
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// ---- Pre-Count and Live Status ----
|
|
663
|
+
suite
|
|
664
|
+
(
|
|
665
|
+
'Pre-Count and Live Status',
|
|
666
|
+
function()
|
|
667
|
+
{
|
|
668
|
+
this.timeout(120000);
|
|
669
|
+
|
|
670
|
+
test
|
|
671
|
+
(
|
|
672
|
+
'Should run a fresh pipeline and capture pre-count data',
|
|
673
|
+
function(fDone)
|
|
674
|
+
{
|
|
675
|
+
// Reset and re-run to capture pre-count
|
|
676
|
+
fPost('/clone/reset', {},
|
|
677
|
+
(pResetErr) =>
|
|
678
|
+
{
|
|
679
|
+
fPost('/clone/session/configure',
|
|
680
|
+
{ ServerURL: `${_HarnessBase}/1.0/` },
|
|
681
|
+
(pSessErr, pSessData) =>
|
|
682
|
+
{
|
|
683
|
+
Expect(pSessData.Success).to.equal(true);
|
|
684
|
+
|
|
685
|
+
fPost('/clone/schema/fetch', { Schema: _HarnessSchema },
|
|
686
|
+
(pSchemaErr, pSchemaData) =>
|
|
687
|
+
{
|
|
688
|
+
Expect(pSchemaData.Success).to.equal(true);
|
|
689
|
+
|
|
690
|
+
fPost('/clone/schema/deploy', { Tables: [] },
|
|
691
|
+
(pDeployErr, pDeployData) =>
|
|
692
|
+
{
|
|
693
|
+
Expect(pDeployData.Success).to.equal(true);
|
|
694
|
+
|
|
695
|
+
// Start sync with NO record cap so it runs long enough to capture live status
|
|
696
|
+
fPost('/clone/sync/start',
|
|
697
|
+
{ SyncMode: 'Initial', MaxRecordsPerEntity: 0 },
|
|
698
|
+
(pSyncErr, pSyncData) =>
|
|
699
|
+
{
|
|
700
|
+
Expect(pSyncData.Success).to.equal(true);
|
|
701
|
+
|
|
702
|
+
// Poll live-status aggressively to capture pre-count and active sync data
|
|
703
|
+
var tmpLiveStatusSamples = [];
|
|
704
|
+
var tmpPollCount = 0;
|
|
705
|
+
var tmpMaxPolls = 60;
|
|
706
|
+
|
|
707
|
+
var fPollLive = function()
|
|
708
|
+
{
|
|
709
|
+
tmpPollCount++;
|
|
710
|
+
fGet('/clone/sync/live-status',
|
|
711
|
+
(pLiveErr, pLiveData) =>
|
|
712
|
+
{
|
|
713
|
+
if (!pLiveErr && pLiveData)
|
|
714
|
+
{
|
|
715
|
+
tmpLiveStatusSamples.push(pLiveData);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Check if sync is done
|
|
719
|
+
fGet('/clone/sync/status',
|
|
720
|
+
(pStatusErr, pStatusData) =>
|
|
721
|
+
{
|
|
722
|
+
if (pStatusData && !pStatusData.Running)
|
|
723
|
+
{
|
|
724
|
+
// Verify we captured some live-status data
|
|
725
|
+
Expect(tmpLiveStatusSamples.length).to.be.greaterThan(0);
|
|
726
|
+
|
|
727
|
+
// Check for syncing samples
|
|
728
|
+
var tmpSyncingSamples = tmpLiveStatusSamples.filter(
|
|
729
|
+
(s) => s.Phase === 'syncing');
|
|
730
|
+
|
|
731
|
+
if (tmpSyncingSamples.length > 0)
|
|
732
|
+
{
|
|
733
|
+
// Verify pre-count data in syncing samples
|
|
734
|
+
var tmpWithPreCount = tmpSyncingSamples.filter(
|
|
735
|
+
(s) => s.PreCountGrandTotal > 0);
|
|
736
|
+
Expect(tmpWithPreCount.length).to.be.greaterThan(0,
|
|
737
|
+
'Expected at least one live-status sample with PreCountGrandTotal > 0');
|
|
738
|
+
|
|
739
|
+
// Verify live-status structure
|
|
740
|
+
var tmpSample = tmpSyncingSamples[tmpSyncingSamples.length - 1];
|
|
741
|
+
Expect(tmpSample).to.have.property('Phase');
|
|
742
|
+
Expect(tmpSample).to.have.property('Message');
|
|
743
|
+
Expect(tmpSample).to.have.property('TotalSynced');
|
|
744
|
+
Expect(tmpSample).to.have.property('TotalRecords');
|
|
745
|
+
Expect(tmpSample).to.have.property('Elapsed');
|
|
746
|
+
Expect(tmpSample).to.have.property('SyncMode');
|
|
747
|
+
}
|
|
748
|
+
else
|
|
749
|
+
{
|
|
750
|
+
// Sync completed before we could sample — verify via report
|
|
751
|
+
fGet('/clone/sync/report',
|
|
752
|
+
(pRepErr, pReport) =>
|
|
753
|
+
{
|
|
754
|
+
Expect(pReport).to.be.an('object');
|
|
755
|
+
Expect(pReport.Summary.TotalSynced).to.be.greaterThan(0);
|
|
756
|
+
Expect(pReport.EventLog).to.be.an('array');
|
|
757
|
+
// Verify pre-count event exists
|
|
758
|
+
var tmpPreCountEvents = pReport.EventLog.filter(
|
|
759
|
+
(e) => e.Type === 'PreCountComplete');
|
|
760
|
+
Expect(tmpPreCountEvents.length).to.be.greaterThan(0);
|
|
761
|
+
return fDone();
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return fDone();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (tmpPollCount >= tmpMaxPolls)
|
|
770
|
+
{
|
|
771
|
+
return fDone(new Error('Live status polling exceeded max polls'));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
setTimeout(fPollLive, 500);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// Start polling immediately
|
|
780
|
+
fPollLive();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// ---- Ongoing Sync ----
|
|
792
|
+
suite
|
|
793
|
+
(
|
|
794
|
+
'Ongoing Sync',
|
|
795
|
+
function()
|
|
796
|
+
{
|
|
797
|
+
this.timeout(120000);
|
|
798
|
+
|
|
799
|
+
test
|
|
800
|
+
(
|
|
801
|
+
'Should run ongoing sync after initial',
|
|
802
|
+
function(fDone)
|
|
803
|
+
{
|
|
804
|
+
fPost('/clone/sync/start',
|
|
805
|
+
{
|
|
806
|
+
SyncMode: 'Ongoing',
|
|
807
|
+
MaxRecordsPerEntity: 50
|
|
808
|
+
},
|
|
809
|
+
(pError, pData) =>
|
|
810
|
+
{
|
|
811
|
+
Expect(pError).to.equal(null);
|
|
812
|
+
Expect(pData.Success).to.equal(true);
|
|
813
|
+
|
|
814
|
+
fWaitForSyncComplete(
|
|
815
|
+
(pWaitErr, pStatus) =>
|
|
816
|
+
{
|
|
817
|
+
Expect(pWaitErr).to.equal(null);
|
|
818
|
+
Expect(pStatus.Running).to.equal(false);
|
|
819
|
+
|
|
820
|
+
var tmpTableNames = Object.keys(pStatus.Tables);
|
|
821
|
+
for (var i = 0; i < tmpTableNames.length; i++)
|
|
822
|
+
{
|
|
823
|
+
var tmpTable = pStatus.Tables[tmpTableNames[i]];
|
|
824
|
+
Expect(['Complete', 'Partial']).to.include(tmpTable.Status,
|
|
825
|
+
`Ongoing sync: ${tmpTableNames[i]} has status ${tmpTable.Status}`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
fDone();
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
test
|
|
834
|
+
(
|
|
835
|
+
'Ongoing sync report should show success',
|
|
836
|
+
function(fDone)
|
|
837
|
+
{
|
|
838
|
+
fGet('/clone/sync/report',
|
|
839
|
+
(pError, pData) =>
|
|
840
|
+
{
|
|
841
|
+
Expect(pError).to.equal(null);
|
|
842
|
+
Expect(['Success', 'Partial']).to.include(pData.Outcome);
|
|
843
|
+
fDone();
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// ---- Stop Sync ----
|
|
851
|
+
suite
|
|
852
|
+
(
|
|
853
|
+
'Stop Sync',
|
|
854
|
+
function()
|
|
855
|
+
{
|
|
856
|
+
this.timeout(120000);
|
|
857
|
+
|
|
858
|
+
test
|
|
859
|
+
(
|
|
860
|
+
'Should be able to stop a sync in progress',
|
|
861
|
+
function(fDone)
|
|
862
|
+
{
|
|
863
|
+
// Reset and start fresh
|
|
864
|
+
fPost('/clone/reset', {},
|
|
865
|
+
() =>
|
|
866
|
+
{
|
|
867
|
+
fPost('/clone/session/configure',
|
|
868
|
+
{ ServerURL: `${_HarnessBase}/1.0/` },
|
|
869
|
+
() =>
|
|
870
|
+
{
|
|
871
|
+
fPost('/clone/schema/fetch', { Schema: _HarnessSchema },
|
|
872
|
+
() =>
|
|
873
|
+
{
|
|
874
|
+
fPost('/clone/schema/deploy', { Tables: [] },
|
|
875
|
+
() =>
|
|
876
|
+
{
|
|
877
|
+
// Start sync with NO record cap (will take a while)
|
|
878
|
+
fPost('/clone/sync/start',
|
|
879
|
+
{ SyncMode: 'Initial', MaxRecordsPerEntity: 0 },
|
|
880
|
+
(pSyncErr, pSyncData) =>
|
|
881
|
+
{
|
|
882
|
+
Expect(pSyncData.Success).to.equal(true);
|
|
883
|
+
|
|
884
|
+
// Wait a moment, then stop
|
|
885
|
+
setTimeout(() =>
|
|
886
|
+
{
|
|
887
|
+
fPost('/clone/sync/stop', {},
|
|
888
|
+
(pStopErr, pStopData) =>
|
|
889
|
+
{
|
|
890
|
+
Expect(pStopData.Success).to.equal(true);
|
|
891
|
+
|
|
892
|
+
// Wait for sync to actually stop
|
|
893
|
+
var fPollStop = function()
|
|
894
|
+
{
|
|
895
|
+
fGet('/clone/sync/status',
|
|
896
|
+
(pPollErr, pPollData) =>
|
|
897
|
+
{
|
|
898
|
+
if (pPollData && !pPollData.Running)
|
|
899
|
+
{
|
|
900
|
+
// Get report
|
|
901
|
+
fGet('/clone/sync/report',
|
|
902
|
+
(pRepErr, pReport) =>
|
|
903
|
+
{
|
|
904
|
+
if (pReport && pReport.Outcome)
|
|
905
|
+
{
|
|
906
|
+
Expect(['Stopped', 'Partial', 'Success']).to.include(pReport.Outcome);
|
|
907
|
+
}
|
|
908
|
+
return fDone();
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
else
|
|
912
|
+
{
|
|
913
|
+
setTimeout(fPollStop, 1000);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
};
|
|
917
|
+
setTimeout(fPollStop, 1000);
|
|
918
|
+
});
|
|
919
|
+
}, 3000);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
// ---- Reset ----
|
|
931
|
+
suite
|
|
932
|
+
(
|
|
933
|
+
'Reset',
|
|
934
|
+
function()
|
|
935
|
+
{
|
|
936
|
+
this.timeout(30000);
|
|
937
|
+
|
|
938
|
+
test
|
|
939
|
+
(
|
|
940
|
+
'Should reset the database',
|
|
941
|
+
function(fDone)
|
|
942
|
+
{
|
|
943
|
+
fPost('/clone/reset', {},
|
|
944
|
+
(pError, pData) =>
|
|
945
|
+
{
|
|
946
|
+
Expect(pError).to.equal(null);
|
|
947
|
+
// After reset, status should show no tables
|
|
948
|
+
fGet('/clone/sync/status',
|
|
949
|
+
(pStatusErr, pStatusData) =>
|
|
950
|
+
{
|
|
951
|
+
Expect(pStatusData.Running).to.equal(false);
|
|
952
|
+
fDone();
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
test
|
|
958
|
+
(
|
|
959
|
+
'Connection should still work after reset',
|
|
960
|
+
function(fDone)
|
|
961
|
+
{
|
|
962
|
+
fGet('/clone/connection/status',
|
|
963
|
+
(pError, pData) =>
|
|
964
|
+
{
|
|
965
|
+
Expect(pError).to.equal(null);
|
|
966
|
+
Expect(pData.Connected).to.equal(true);
|
|
967
|
+
fDone();
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
// ---- Storage Engine: MySQL (conditional) ----
|
|
975
|
+
suite
|
|
976
|
+
(
|
|
977
|
+
'Storage Engine: MySQL',
|
|
978
|
+
function()
|
|
979
|
+
{
|
|
980
|
+
this.timeout(120000);
|
|
981
|
+
|
|
982
|
+
var _MysqlAvailable = false;
|
|
983
|
+
|
|
984
|
+
suiteSetup
|
|
985
|
+
(
|
|
986
|
+
function()
|
|
987
|
+
{
|
|
988
|
+
if (_RequestedEngines.indexOf('mysql') < 0 || !process.env.MYSQL_HOST)
|
|
989
|
+
{
|
|
990
|
+
_EngineResults['MySQL'] = { status: 'skip', reason: 'MYSQL_HOST not set' };
|
|
991
|
+
this.skip();
|
|
992
|
+
}
|
|
993
|
+
else
|
|
994
|
+
{
|
|
995
|
+
_MysqlAvailable = true;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
test
|
|
1001
|
+
(
|
|
1002
|
+
'Should sync via MySQL',
|
|
1003
|
+
function(fDone)
|
|
1004
|
+
{
|
|
1005
|
+
if (!_MysqlAvailable) return this.skip();
|
|
1006
|
+
|
|
1007
|
+
var tmpConfig =
|
|
1008
|
+
{
|
|
1009
|
+
Provider: 'MySQL',
|
|
1010
|
+
Config:
|
|
1011
|
+
{
|
|
1012
|
+
Server: process.env.MYSQL_HOST || 'localhost',
|
|
1013
|
+
Port: parseInt(process.env.MYSQL_PORT, 10) || 3306,
|
|
1014
|
+
User: process.env.MYSQL_USER || 'root',
|
|
1015
|
+
Password: process.env.MYSQL_PASSWORD || '',
|
|
1016
|
+
Database: process.env.MYSQL_DATABASE || 'retold_cloner_test',
|
|
1017
|
+
ConnectionPoolLimit: 5
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
fRunFullPipeline(tmpConfig, 20,
|
|
1022
|
+
(pError, pResult) =>
|
|
1023
|
+
{
|
|
1024
|
+
if (pError)
|
|
1025
|
+
{
|
|
1026
|
+
_EngineResults['MySQL'] = { status: 'fail', error: pError.message };
|
|
1027
|
+
return fDone(pError);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
Expect(pResult.report).to.be.an('object');
|
|
1031
|
+
Expect(['Success', 'Partial']).to.include(pResult.report.Outcome);
|
|
1032
|
+
|
|
1033
|
+
_EngineResults['MySQL'] =
|
|
1034
|
+
{
|
|
1035
|
+
status: 'pass',
|
|
1036
|
+
sync_duration_ms: pResult.syncDuration,
|
|
1037
|
+
records_synced: pResult.report.Summary.TotalSynced || 0,
|
|
1038
|
+
tables_synced: pResult.report.Summary.TotalTables || 0
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
fDone();
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
// ---- Storage Engine: PostgreSQL (conditional) ----
|
|
1049
|
+
suite
|
|
1050
|
+
(
|
|
1051
|
+
'Storage Engine: PostgreSQL',
|
|
1052
|
+
function()
|
|
1053
|
+
{
|
|
1054
|
+
this.timeout(120000);
|
|
1055
|
+
|
|
1056
|
+
var _PgAvailable = false;
|
|
1057
|
+
|
|
1058
|
+
suiteSetup
|
|
1059
|
+
(
|
|
1060
|
+
function()
|
|
1061
|
+
{
|
|
1062
|
+
if (_RequestedEngines.indexOf('postgresql') < 0 || !process.env.POSTGRESQL_HOST)
|
|
1063
|
+
{
|
|
1064
|
+
_EngineResults['PostgreSQL'] = { status: 'skip', reason: 'POSTGRESQL_HOST not set' };
|
|
1065
|
+
this.skip();
|
|
1066
|
+
}
|
|
1067
|
+
else
|
|
1068
|
+
{
|
|
1069
|
+
_PgAvailable = true;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
test
|
|
1075
|
+
(
|
|
1076
|
+
'Should sync via PostgreSQL',
|
|
1077
|
+
function(fDone)
|
|
1078
|
+
{
|
|
1079
|
+
if (!_PgAvailable) return this.skip();
|
|
1080
|
+
|
|
1081
|
+
var tmpConfig =
|
|
1082
|
+
{
|
|
1083
|
+
Provider: 'PostgreSQL',
|
|
1084
|
+
Config:
|
|
1085
|
+
{
|
|
1086
|
+
Server: process.env.POSTGRESQL_HOST || 'localhost',
|
|
1087
|
+
Port: parseInt(process.env.POSTGRESQL_PORT, 10) || 5432,
|
|
1088
|
+
User: process.env.POSTGRESQL_USER || 'postgres',
|
|
1089
|
+
Password: process.env.POSTGRESQL_PASSWORD || '',
|
|
1090
|
+
Database: process.env.POSTGRESQL_DATABASE || 'retold_cloner_test',
|
|
1091
|
+
ConnectionPoolLimit: 5
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
fRunFullPipeline(tmpConfig, 20,
|
|
1096
|
+
(pError, pResult) =>
|
|
1097
|
+
{
|
|
1098
|
+
if (pError)
|
|
1099
|
+
{
|
|
1100
|
+
_EngineResults['PostgreSQL'] = { status: 'fail', error: pError.message };
|
|
1101
|
+
return fDone(pError);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
Expect(pResult.report).to.be.an('object');
|
|
1105
|
+
Expect(['Success', 'Partial']).to.include(pResult.report.Outcome);
|
|
1106
|
+
|
|
1107
|
+
_EngineResults['PostgreSQL'] =
|
|
1108
|
+
{
|
|
1109
|
+
status: 'pass',
|
|
1110
|
+
sync_duration_ms: pResult.syncDuration,
|
|
1111
|
+
records_synced: pResult.report.Summary.TotalSynced || 0,
|
|
1112
|
+
tables_synced: pResult.report.Summary.TotalTables || 0
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
fDone();
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
// ---- Storage Engine: MSSQL (conditional) ----
|
|
1123
|
+
suite
|
|
1124
|
+
(
|
|
1125
|
+
'Storage Engine: MSSQL',
|
|
1126
|
+
function()
|
|
1127
|
+
{
|
|
1128
|
+
this.timeout(120000);
|
|
1129
|
+
|
|
1130
|
+
var _MssqlAvailable = false;
|
|
1131
|
+
|
|
1132
|
+
suiteSetup
|
|
1133
|
+
(
|
|
1134
|
+
function()
|
|
1135
|
+
{
|
|
1136
|
+
if (_RequestedEngines.indexOf('mssql') < 0 || !process.env.MSSQL_HOST)
|
|
1137
|
+
{
|
|
1138
|
+
_EngineResults['MSSQL'] = { status: 'skip', reason: 'MSSQL_HOST not set' };
|
|
1139
|
+
this.skip();
|
|
1140
|
+
}
|
|
1141
|
+
else
|
|
1142
|
+
{
|
|
1143
|
+
_MssqlAvailable = true;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
test
|
|
1149
|
+
(
|
|
1150
|
+
'Should sync via MSSQL',
|
|
1151
|
+
function(fDone)
|
|
1152
|
+
{
|
|
1153
|
+
if (!_MssqlAvailable) return this.skip();
|
|
1154
|
+
|
|
1155
|
+
var tmpConfig =
|
|
1156
|
+
{
|
|
1157
|
+
Provider: 'MSSQL',
|
|
1158
|
+
Config:
|
|
1159
|
+
{
|
|
1160
|
+
Server: process.env.MSSQL_HOST || 'localhost',
|
|
1161
|
+
Port: parseInt(process.env.MSSQL_PORT, 10) || 1433,
|
|
1162
|
+
User: process.env.MSSQL_USER || 'sa',
|
|
1163
|
+
Password: process.env.MSSQL_PASSWORD || '',
|
|
1164
|
+
Database: process.env.MSSQL_DATABASE || 'retold_cloner_test'
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
fRunFullPipeline(tmpConfig, 20,
|
|
1169
|
+
(pError, pResult) =>
|
|
1170
|
+
{
|
|
1171
|
+
if (pError)
|
|
1172
|
+
{
|
|
1173
|
+
_EngineResults['MSSQL'] = { status: 'fail', error: pError.message };
|
|
1174
|
+
return fDone(pError);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
Expect(pResult.report).to.be.an('object');
|
|
1178
|
+
Expect(['Success', 'Partial']).to.include(pResult.report.Outcome);
|
|
1179
|
+
|
|
1180
|
+
_EngineResults['MSSQL'] =
|
|
1181
|
+
{
|
|
1182
|
+
status: 'pass',
|
|
1183
|
+
sync_duration_ms: pResult.syncDuration,
|
|
1184
|
+
records_synced: pResult.report.Summary.TotalSynced || 0,
|
|
1185
|
+
tables_synced: pResult.report.Summary.TotalTables || 0
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
fDone();
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
// ---- Expose engine results for the runner ----
|
|
1196
|
+
suiteTeardown
|
|
1197
|
+
(
|
|
1198
|
+
function()
|
|
1199
|
+
{
|
|
1200
|
+
// Write engine results to env so the runner can pick them up
|
|
1201
|
+
process.env.ENGINE_RESULTS = JSON.stringify(_EngineResults);
|
|
1202
|
+
}
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
);
|