retold-data-service 2.0.18 → 2.0.19
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/package.json +2 -2
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +12 -2
- package/source/services/data-cloner/DataCloner-Command-Schema.js +4 -3
- package/source/services/data-cloner/DataCloner-Command-Sync.js +16 -3
- package/source/services/data-cloner/DataCloner-ProviderRegistry.js +1 -1
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +47 -7
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +2 -1
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +86 -2
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +28 -0
- package/source/services/data-cloner/web/data-cloner.js +95 -3
- package/source/services/data-cloner/web/data-cloner.js.map +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
- package/test/integration-report.json +35 -116
- package/test/run-integration-tests.js +52 -12
- package/test/run-integration-tests.sh +221 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"timestamp": "2026-03-
|
|
3
|
-
"duration_ms":
|
|
2
|
+
"timestamp": "2026-03-09T04:55:24.920Z",
|
|
3
|
+
"duration_ms": 22241,
|
|
4
4
|
"summary": {
|
|
5
|
-
"total":
|
|
6
|
-
"passed":
|
|
5
|
+
"total": 26,
|
|
6
|
+
"passed": 26,
|
|
7
7
|
"failed": 0,
|
|
8
|
-
"skipped":
|
|
8
|
+
"skipped": 0
|
|
9
9
|
},
|
|
10
10
|
"storage_engines": {},
|
|
11
11
|
"suites": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"name": "Should show initial connection status",
|
|
22
22
|
"status": "pass",
|
|
23
|
-
"duration_ms":
|
|
23
|
+
"duration_ms": 2
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "Should connect SQLite via configure",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
{
|
|
31
31
|
"name": "Should show connected after configure",
|
|
32
32
|
"status": "pass",
|
|
33
|
-
"duration_ms":
|
|
33
|
+
"duration_ms": 1
|
|
34
34
|
}
|
|
35
35
|
],
|
|
36
|
-
"duration_ms":
|
|
36
|
+
"duration_ms": 5
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "Session Configuration",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
{
|
|
47
47
|
"name": "Should show session as configured",
|
|
48
48
|
"status": "pass",
|
|
49
|
-
"duration_ms":
|
|
49
|
+
"duration_ms": 10
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"name": "Should reject session configure without ServerURL",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"duration_ms": 1
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
|
-
"duration_ms":
|
|
57
|
+
"duration_ms": 12
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"name": "Schema Fetch",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
{
|
|
90
90
|
"name": "Should start initial sync with record cap",
|
|
91
91
|
"status": "pass",
|
|
92
|
-
"duration_ms":
|
|
92
|
+
"duration_ms": 1005
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"name": "All tables should have completed",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
{
|
|
100
100
|
"name": "Should have a valid sync report",
|
|
101
101
|
"status": "pass",
|
|
102
|
-
"duration_ms":
|
|
102
|
+
"duration_ms": 2
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
"name": "Report tables should have timing data",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"duration_ms": 1
|
|
108
108
|
}
|
|
109
109
|
],
|
|
110
|
-
"duration_ms":
|
|
110
|
+
"duration_ms": 1009
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"name": "Data Integrity",
|
|
@@ -115,12 +115,12 @@
|
|
|
115
115
|
{
|
|
116
116
|
"name": "Should fetch harness reference data",
|
|
117
117
|
"status": "pass",
|
|
118
|
-
"duration_ms":
|
|
118
|
+
"duration_ms": 7
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"name": "Local book count should match sync (capped)",
|
|
122
122
|
"status": "pass",
|
|
123
|
-
"duration_ms":
|
|
123
|
+
"duration_ms": 1
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
"name": "Local Book 1 should match harness data",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"duration_ms": 0
|
|
134
134
|
}
|
|
135
135
|
],
|
|
136
|
-
"duration_ms":
|
|
136
|
+
"duration_ms": 9
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "Pre-Count and Live Status",
|
|
@@ -141,10 +141,10 @@
|
|
|
141
141
|
{
|
|
142
142
|
"name": "Should run a fresh pipeline and capture pre-count data",
|
|
143
143
|
"status": "pass",
|
|
144
|
-
"duration_ms":
|
|
144
|
+
"duration_ms": 8863
|
|
145
145
|
}
|
|
146
146
|
],
|
|
147
|
-
"duration_ms":
|
|
147
|
+
"duration_ms": 8863
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"name": "Ongoing Sync",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
{
|
|
153
153
|
"name": "Should run ongoing sync after initial",
|
|
154
154
|
"status": "pass",
|
|
155
|
-
"duration_ms":
|
|
155
|
+
"duration_ms": 1015
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"name": "Ongoing sync report should show success",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"duration_ms": 1
|
|
161
161
|
}
|
|
162
162
|
],
|
|
163
|
-
"duration_ms":
|
|
163
|
+
"duration_ms": 1016
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"name": "Stop Sync",
|
|
@@ -168,10 +168,10 @@
|
|
|
168
168
|
{
|
|
169
169
|
"name": "Should be able to stop a sync in progress",
|
|
170
170
|
"status": "pass",
|
|
171
|
-
"duration_ms":
|
|
171
|
+
"duration_ms": 4020
|
|
172
172
|
}
|
|
173
173
|
],
|
|
174
|
-
"duration_ms":
|
|
174
|
+
"duration_ms": 4020
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
"name": "Reset",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
{
|
|
180
180
|
"name": "Should reset the database",
|
|
181
181
|
"status": "pass",
|
|
182
|
-
"duration_ms":
|
|
182
|
+
"duration_ms": 1
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"name": "Connection should still work after reset",
|
|
@@ -187,125 +187,44 @@
|
|
|
187
187
|
"duration_ms": 1
|
|
188
188
|
}
|
|
189
189
|
],
|
|
190
|
-
"duration_ms":
|
|
190
|
+
"duration_ms": 2
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"name": "Storage Engine: MySQL",
|
|
194
194
|
"tests": [
|
|
195
195
|
{
|
|
196
196
|
"name": "Should sync via MySQL",
|
|
197
|
-
"status": "
|
|
198
|
-
"duration_ms":
|
|
197
|
+
"status": "pass",
|
|
198
|
+
"duration_ms": 1033
|
|
199
199
|
}
|
|
200
200
|
],
|
|
201
|
-
"duration_ms":
|
|
201
|
+
"duration_ms": 1033
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
"name": "Storage Engine: PostgreSQL",
|
|
205
205
|
"tests": [
|
|
206
206
|
{
|
|
207
207
|
"name": "Should sync via PostgreSQL",
|
|
208
|
-
"status": "
|
|
209
|
-
"duration_ms":
|
|
208
|
+
"status": "pass",
|
|
209
|
+
"duration_ms": 1035
|
|
210
210
|
}
|
|
211
211
|
],
|
|
212
|
-
"duration_ms":
|
|
212
|
+
"duration_ms": 1035
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"name": "Storage Engine: MSSQL",
|
|
216
216
|
"tests": [
|
|
217
217
|
{
|
|
218
218
|
"name": "Should sync via MSSQL",
|
|
219
|
-
"status": "
|
|
220
|
-
"duration_ms":
|
|
219
|
+
"status": "pass",
|
|
220
|
+
"duration_ms": 3103
|
|
221
221
|
}
|
|
222
222
|
],
|
|
223
|
-
"duration_ms":
|
|
223
|
+
"duration_ms": 3103
|
|
224
224
|
}
|
|
225
225
|
],
|
|
226
226
|
"puppeteer": {
|
|
227
|
-
"available":
|
|
228
|
-
"suites": [
|
|
229
|
-
{
|
|
230
|
-
"name": "Data Cloner Web UI (Puppeteer)",
|
|
231
|
-
"tests": [],
|
|
232
|
-
"duration_ms": 0
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
"name": "Web UI Load",
|
|
236
|
-
"tests": [
|
|
237
|
-
{
|
|
238
|
-
"name": "Should load the data cloner page",
|
|
239
|
-
"status": "pass",
|
|
240
|
-
"duration_ms": 734
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
"name": "Should display 7 accordion sections",
|
|
244
|
-
"status": "pass",
|
|
245
|
-
"duration_ms": 15
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
"name": "Should show live status bar",
|
|
249
|
-
"status": "pass",
|
|
250
|
-
"duration_ms": 201
|
|
251
|
-
}
|
|
252
|
-
],
|
|
253
|
-
"duration_ms": 950
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"name": "Connection Section",
|
|
257
|
-
"tests": [
|
|
258
|
-
{
|
|
259
|
-
"name": "Should connect to SQLite via go button",
|
|
260
|
-
"status": "pass",
|
|
261
|
-
"duration_ms": 2218
|
|
262
|
-
}
|
|
263
|
-
],
|
|
264
|
-
"duration_ms": 2218
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
"name": "Session, Schema, and Deploy Flow",
|
|
268
|
-
"tests": [
|
|
269
|
-
{
|
|
270
|
-
"name": "Should configure session via UI",
|
|
271
|
-
"status": "pass",
|
|
272
|
-
"duration_ms": 3019
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
"name": "Should fetch schema via UI",
|
|
276
|
-
"status": "pass",
|
|
277
|
-
"duration_ms": 2020
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
"name": "Should deploy schema via UI",
|
|
281
|
-
"status": "pass",
|
|
282
|
-
"duration_ms": 5074
|
|
283
|
-
}
|
|
284
|
-
],
|
|
285
|
-
"duration_ms": 10113
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
"name": "Sync via Web UI",
|
|
289
|
-
"tests": [
|
|
290
|
-
{
|
|
291
|
-
"name": "Should start sync and complete successfully",
|
|
292
|
-
"status": "pass",
|
|
293
|
-
"duration_ms": 7184
|
|
294
|
-
}
|
|
295
|
-
],
|
|
296
|
-
"duration_ms": 7184
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
"name": "Live Status Display",
|
|
300
|
-
"tests": [
|
|
301
|
-
{
|
|
302
|
-
"name": "Should show progress information during sync",
|
|
303
|
-
"status": "pass",
|
|
304
|
-
"duration_ms": 16243
|
|
305
|
-
}
|
|
306
|
-
],
|
|
307
|
-
"duration_ms": 16243
|
|
308
|
-
}
|
|
309
|
-
]
|
|
227
|
+
"available": false,
|
|
228
|
+
"suites": []
|
|
310
229
|
}
|
|
311
230
|
}
|
|
@@ -166,24 +166,54 @@ function fPrintReport()
|
|
|
166
166
|
console.log(` Skipped: ${_Report.summary.skipped}`);
|
|
167
167
|
console.log('-'.repeat(72));
|
|
168
168
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
let
|
|
172
|
-
for (let i = 0; i < tmpEngines.length; i++)
|
|
169
|
+
// Build storage engine status from suite results
|
|
170
|
+
let tmpStorageEnginePrefix = 'Storage Engine: ';
|
|
171
|
+
for (let i = 0; i < _Report.suites.length; i++)
|
|
173
172
|
{
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
let tmpSuite = _Report.suites[i];
|
|
174
|
+
if (tmpSuite.name.indexOf(tmpStorageEnginePrefix) !== 0) continue;
|
|
175
|
+
|
|
176
|
+
let tmpEngineName = tmpSuite.name.substring(tmpStorageEnginePrefix.length);
|
|
177
|
+
let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
|
|
178
|
+
let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
|
|
179
|
+
let tmpSkipCount = tmpSuite.tests.filter((t) => t.status === 'skip').length;
|
|
180
|
+
|
|
181
|
+
if (tmpSkipCount === tmpSuite.tests.length)
|
|
177
182
|
{
|
|
178
|
-
|
|
183
|
+
_Report.storage_engines[tmpEngineName] = { status: 'skip', reason: 'Not configured (env vars not set)' };
|
|
179
184
|
}
|
|
180
|
-
else if (
|
|
185
|
+
else if (tmpFailCount > 0)
|
|
181
186
|
{
|
|
182
|
-
|
|
187
|
+
let tmpFirstError = tmpSuite.tests.find((t) => t.status === 'fail');
|
|
188
|
+
_Report.storage_engines[tmpEngineName] = { status: 'fail', error: tmpFirstError ? tmpFirstError.error : 'Unknown' };
|
|
183
189
|
}
|
|
184
190
|
else
|
|
185
191
|
{
|
|
186
|
-
|
|
192
|
+
_Report.storage_engines[tmpEngineName] = { status: 'pass', sync_duration_ms: tmpSuite.duration_ms, records_synced: 0, tables_synced: 0 };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Storage engines
|
|
197
|
+
let tmpEngines = Object.keys(_Report.storage_engines);
|
|
198
|
+
if (tmpEngines.length > 0)
|
|
199
|
+
{
|
|
200
|
+
console.log('\n Storage Engines:');
|
|
201
|
+
for (let i = 0; i < tmpEngines.length; i++)
|
|
202
|
+
{
|
|
203
|
+
let tmpName = tmpEngines[i];
|
|
204
|
+
let tmpEngine = _Report.storage_engines[tmpName];
|
|
205
|
+
if (tmpEngine.status === 'pass')
|
|
206
|
+
{
|
|
207
|
+
console.log(` ${tmpName.padEnd(14)} PASS ${fFormatDuration(tmpEngine.sync_duration_ms).padStart(10)} ${tmpEngine.records_synced} records / ${tmpEngine.tables_synced} tables`);
|
|
208
|
+
}
|
|
209
|
+
else if (tmpEngine.status === 'fail')
|
|
210
|
+
{
|
|
211
|
+
console.log(` ${tmpName.padEnd(14)} FAIL ${tmpEngine.error || 'Unknown error'}`);
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
{
|
|
215
|
+
console.log(` ${tmpName.padEnd(14)} SKIP ${tmpEngine.reason || ''}`);
|
|
216
|
+
}
|
|
187
217
|
}
|
|
188
218
|
}
|
|
189
219
|
|
|
@@ -194,8 +224,18 @@ function fPrintReport()
|
|
|
194
224
|
let tmpSuite = _Report.suites[i];
|
|
195
225
|
let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
|
|
196
226
|
let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
|
|
227
|
+
let tmpSkipCount = tmpSuite.tests.filter((t) => t.status === 'skip').length;
|
|
228
|
+
|
|
229
|
+
if (tmpSkipCount === tmpSuite.tests.length && tmpSuite.tests.length > 0)
|
|
230
|
+
{
|
|
231
|
+
// All tests in this suite were skipped
|
|
232
|
+
console.log(` ~ ${tmpSuite.name.padEnd(35)} skipped`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
197
236
|
let tmpStatus = tmpFailCount > 0 ? 'FAIL' : 'PASS';
|
|
198
|
-
|
|
237
|
+
let tmpSkipLabel = tmpSkipCount > 0 ? ` (${tmpSkipCount} skipped)` : '';
|
|
238
|
+
console.log(` ${tmpStatus === 'FAIL' ? 'X' : '+'} ${tmpSuite.name.padEnd(35)} ${tmpPassCount}/${tmpPassCount + tmpFailCount} passed ${fFormatDuration(tmpSuite.duration_ms)}${tmpSkipLabel}`);
|
|
199
239
|
|
|
200
240
|
// Show failed tests
|
|
201
241
|
for (let j = 0; j < tmpSuite.tests.length; j++)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ==============================================================================
|
|
3
|
+
# Data Cloner Integration Tests — All Storage Engines
|
|
4
|
+
#
|
|
5
|
+
# Starts Docker containers for MySQL, PostgreSQL, and MSSQL, creates the
|
|
6
|
+
# required test databases, then runs the full integration test suite against
|
|
7
|
+
# every engine (SQLite runs without Docker).
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ./test/run-integration-tests.sh # All engines
|
|
11
|
+
# ./test/run-integration-tests.sh sqlite # SQLite only (no Docker)
|
|
12
|
+
# ./test/run-integration-tests.sh mysql # MySQL only
|
|
13
|
+
# ./test/run-integration-tests.sh mysql,mssql # Specific engines
|
|
14
|
+
#
|
|
15
|
+
# Prerequisites:
|
|
16
|
+
# - Docker (and docker compose) installed and running
|
|
17
|
+
# - Node.js and npm
|
|
18
|
+
# - npm install already run in retold-data-service
|
|
19
|
+
# ==============================================================================
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
25
|
+
HARNESS_DIR="$(cd "$PROJECT_DIR/../retold-harness" 2>/dev/null && pwd)" || true
|
|
26
|
+
|
|
27
|
+
# ---- Parse arguments ----
|
|
28
|
+
ENGINES="${1:-sqlite,mysql,postgresql,mssql}"
|
|
29
|
+
|
|
30
|
+
# ---- Credentials (match retold-harness/docker-compose.yml) ----
|
|
31
|
+
MYSQL_ROOT_PASSWORD="1234567890"
|
|
32
|
+
MYSQL_TEST_DB="retold_cloner_test"
|
|
33
|
+
|
|
34
|
+
POSTGRESQL_PASSWORD="retold1234567890"
|
|
35
|
+
POSTGRESQL_TEST_DB="retold_cloner_test"
|
|
36
|
+
|
|
37
|
+
MSSQL_SA_PASSWORD="Retold1234567890!"
|
|
38
|
+
MSSQL_TEST_DB="retold_cloner_test"
|
|
39
|
+
|
|
40
|
+
# ---- Colours ----
|
|
41
|
+
RED='\033[0;31m'
|
|
42
|
+
GREEN='\033[0;32m'
|
|
43
|
+
YELLOW='\033[1;33m'
|
|
44
|
+
CYAN='\033[0;36m'
|
|
45
|
+
NC='\033[0m' # No colour
|
|
46
|
+
|
|
47
|
+
log() { echo -e "${CYAN}[$(date +%H:%M:%S)]${NC} $*"; }
|
|
48
|
+
warn() { echo -e "${YELLOW}[$(date +%H:%M:%S)] WARNING:${NC} $*"; }
|
|
49
|
+
err() { echo -e "${RED}[$(date +%H:%M:%S)] ERROR:${NC} $*"; }
|
|
50
|
+
ok() { echo -e "${GREEN}[$(date +%H:%M:%S)] OK:${NC} $*"; }
|
|
51
|
+
|
|
52
|
+
needs_docker()
|
|
53
|
+
{
|
|
54
|
+
[[ "$ENGINES" == *mysql* ]] || [[ "$ENGINES" == *postgresql* ]] || [[ "$ENGINES" == *mssql* ]]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ---- Check prerequisites ----
|
|
58
|
+
if needs_docker; then
|
|
59
|
+
if ! command -v docker &>/dev/null; then
|
|
60
|
+
err "Docker is not installed. Install Docker to test non-SQLite engines."
|
|
61
|
+
err "Or run: $0 sqlite"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
if ! docker info &>/dev/null; then
|
|
65
|
+
err "Docker daemon is not running. Start Docker first."
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [ -z "$HARNESS_DIR" ] || [ ! -d "$HARNESS_DIR" ]; then
|
|
71
|
+
err "retold-harness not found at $PROJECT_DIR/../retold-harness"
|
|
72
|
+
err "Make sure retold-harness is cloned as a sibling of retold-data-service."
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# ---- Start Docker containers ----
|
|
77
|
+
start_containers()
|
|
78
|
+
{
|
|
79
|
+
log "Starting Docker containers..."
|
|
80
|
+
cd "$HARNESS_DIR"
|
|
81
|
+
|
|
82
|
+
local SERVICES=""
|
|
83
|
+
[[ "$ENGINES" == *mysql* ]] && SERVICES="$SERVICES mysql"
|
|
84
|
+
[[ "$ENGINES" == *postgresql* ]] && SERVICES="$SERVICES postgresql"
|
|
85
|
+
[[ "$ENGINES" == *mssql* ]] && SERVICES="$SERVICES mssql"
|
|
86
|
+
|
|
87
|
+
if [ -n "$SERVICES" ]; then
|
|
88
|
+
docker compose up -d $SERVICES
|
|
89
|
+
log "Waiting for containers to become healthy..."
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
cd "$PROJECT_DIR"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Wait for a container to be healthy (or timeout after N seconds)
|
|
96
|
+
wait_healthy()
|
|
97
|
+
{
|
|
98
|
+
local CONTAINER="$1"
|
|
99
|
+
local TIMEOUT="${2:-120}"
|
|
100
|
+
local ELAPSED=0
|
|
101
|
+
|
|
102
|
+
while [ $ELAPSED -lt $TIMEOUT ]; do
|
|
103
|
+
local STATUS
|
|
104
|
+
STATUS="$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null || echo "missing")"
|
|
105
|
+
if [ "$STATUS" = "healthy" ]; then
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
sleep 2
|
|
109
|
+
ELAPSED=$((ELAPSED + 2))
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
err "$CONTAINER did not become healthy within ${TIMEOUT}s (status: $STATUS)"
|
|
113
|
+
return 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ---- Create test databases ----
|
|
117
|
+
create_mysql_db()
|
|
118
|
+
{
|
|
119
|
+
log "Waiting for MySQL to be healthy..."
|
|
120
|
+
wait_healthy "retold-harness-mysql" 120
|
|
121
|
+
|
|
122
|
+
log "Creating MySQL database '${MYSQL_TEST_DB}'..."
|
|
123
|
+
docker exec retold-harness-mysql mysql \
|
|
124
|
+
-uroot "-p${MYSQL_ROOT_PASSWORD}" \
|
|
125
|
+
-e "CREATE DATABASE IF NOT EXISTS \`${MYSQL_TEST_DB}\`;" 2>/dev/null
|
|
126
|
+
ok "MySQL database '${MYSQL_TEST_DB}' ready"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
create_postgresql_db()
|
|
130
|
+
{
|
|
131
|
+
log "Waiting for PostgreSQL to be healthy..."
|
|
132
|
+
wait_healthy "retold-harness-postgresql" 120
|
|
133
|
+
|
|
134
|
+
log "Creating PostgreSQL database '${POSTGRESQL_TEST_DB}'..."
|
|
135
|
+
# createdb returns 0 even if DB exists when using IF NOT EXISTS via psql
|
|
136
|
+
docker exec retold-harness-postgresql psql \
|
|
137
|
+
-U postgres \
|
|
138
|
+
-tc "SELECT 1 FROM pg_database WHERE datname = '${POSTGRESQL_TEST_DB}'" \
|
|
139
|
+
| grep -q 1 \
|
|
140
|
+
|| docker exec retold-harness-postgresql psql \
|
|
141
|
+
-U postgres \
|
|
142
|
+
-c "CREATE DATABASE ${POSTGRESQL_TEST_DB};"
|
|
143
|
+
ok "PostgreSQL database '${POSTGRESQL_TEST_DB}' ready"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
create_mssql_db()
|
|
147
|
+
{
|
|
148
|
+
log "Waiting for MSSQL to be healthy..."
|
|
149
|
+
wait_healthy "retold-harness-mssql" 120
|
|
150
|
+
|
|
151
|
+
log "Creating MSSQL database '${MSSQL_TEST_DB}'..."
|
|
152
|
+
docker exec retold-harness-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
153
|
+
-S localhost -U sa -P "${MSSQL_SA_PASSWORD}" -C \
|
|
154
|
+
-Q "IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = '${MSSQL_TEST_DB}') CREATE DATABASE [${MSSQL_TEST_DB}];" 2>/dev/null
|
|
155
|
+
ok "MSSQL database '${MSSQL_TEST_DB}' ready"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ---- Main ----
|
|
159
|
+
|
|
160
|
+
log "Engines requested: ${ENGINES}"
|
|
161
|
+
log "Project dir: ${PROJECT_DIR}"
|
|
162
|
+
log "Harness dir: ${HARNESS_DIR}"
|
|
163
|
+
echo ""
|
|
164
|
+
|
|
165
|
+
# Start Docker containers if needed
|
|
166
|
+
if needs_docker; then
|
|
167
|
+
start_containers
|
|
168
|
+
|
|
169
|
+
# Create test databases in parallel where possible
|
|
170
|
+
[[ "$ENGINES" == *mysql* ]] && create_mysql_db
|
|
171
|
+
[[ "$ENGINES" == *postgresql* ]] && create_postgresql_db
|
|
172
|
+
[[ "$ENGINES" == *mssql* ]] && create_mssql_db
|
|
173
|
+
|
|
174
|
+
echo ""
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# ---- Set environment variables ----
|
|
178
|
+
if [[ "$ENGINES" == *mysql* ]]; then
|
|
179
|
+
export MYSQL_HOST="localhost"
|
|
180
|
+
export MYSQL_PORT="3306"
|
|
181
|
+
export MYSQL_USER="root"
|
|
182
|
+
export MYSQL_PASSWORD="${MYSQL_ROOT_PASSWORD}"
|
|
183
|
+
export MYSQL_DATABASE="${MYSQL_TEST_DB}"
|
|
184
|
+
ok "MySQL env configured (localhost:3306, db=${MYSQL_TEST_DB})"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
if [[ "$ENGINES" == *postgresql* ]]; then
|
|
188
|
+
export POSTGRESQL_HOST="localhost"
|
|
189
|
+
export POSTGRESQL_PORT="5432"
|
|
190
|
+
export POSTGRESQL_USER="postgres"
|
|
191
|
+
export POSTGRESQL_PASSWORD="${POSTGRESQL_PASSWORD}"
|
|
192
|
+
export POSTGRESQL_DATABASE="${POSTGRESQL_TEST_DB}"
|
|
193
|
+
ok "PostgreSQL env configured (localhost:5432, db=${POSTGRESQL_TEST_DB})"
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
if [[ "$ENGINES" == *mssql* ]]; then
|
|
197
|
+
export MSSQL_HOST="localhost"
|
|
198
|
+
export MSSQL_PORT="1433"
|
|
199
|
+
export MSSQL_USER="sa"
|
|
200
|
+
export MSSQL_PASSWORD="${MSSQL_SA_PASSWORD}"
|
|
201
|
+
export MSSQL_DATABASE="${MSSQL_TEST_DB}"
|
|
202
|
+
ok "MSSQL env configured (localhost:1433, db=${MSSQL_TEST_DB})"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
echo ""
|
|
206
|
+
log "Running integration tests..."
|
|
207
|
+
echo ""
|
|
208
|
+
|
|
209
|
+
# ---- Run tests ----
|
|
210
|
+
cd "$PROJECT_DIR"
|
|
211
|
+
node test/run-integration-tests.js --skip-puppeteer --engines="${ENGINES}"
|
|
212
|
+
TEST_EXIT=$?
|
|
213
|
+
|
|
214
|
+
echo ""
|
|
215
|
+
if [ $TEST_EXIT -eq 0 ]; then
|
|
216
|
+
ok "All integration tests passed!"
|
|
217
|
+
else
|
|
218
|
+
err "Some tests failed (exit code: ${TEST_EXIT})"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
exit $TEST_EXIT
|