@aria-framework/testkit 0.2.0 → 0.3.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.
- package/package.json +1 -1
- package/tempDb.js +58 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aria-framework/testkit",
|
|
3
3
|
"description": "Aria App Framework \u2014 test infrastructure. The \u2713/\u2717 micro test-harness (createHarness, with the async-callback guard that stops a suite silently passing), assertImportsInScope (every consumer of a module has every function it calls in scope), and assertVersionSingleSource (package.json vs derived version files vs the lockfile's two copies). Dev-dependency only: nothing here belongs in a production install.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"publishConfig": {
|
package/tempDb.js
CHANGED
|
@@ -54,6 +54,44 @@ function removeAll(base) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Run a caller's close hook, and refuse to be lied to about it.
|
|
59
|
+
*
|
|
60
|
+
* AN EXIT HANDLER CANNOT AWAIT. So a `close` that returns a promise has not closed anything by the
|
|
61
|
+
* time the unlink runs, and on Windows that unlink then fails with EBUSY into a catch nobody reads.
|
|
62
|
+
* The copy is left behind and the suite still passes — which is precisely the shape of bug this
|
|
63
|
+
* whole module exists to end, arriving through the mechanism meant to prevent it.
|
|
64
|
+
*
|
|
65
|
+
* It is worse than a plain failure because an async close can APPEAR to work: `async close() {
|
|
66
|
+
* db.close(); }` runs its body up to the first await synchronously, so a driver written that way
|
|
67
|
+
* cleans up perfectly until somebody adds an await ahead of that line, or the app moves to a driver
|
|
68
|
+
* whose close is genuinely asynchronous. Then every leftover returns, silently, and the commit that
|
|
69
|
+
* introduced the regression is nowhere near the code that breaks.
|
|
70
|
+
*
|
|
71
|
+
* So: say so, loudly, on the one occasion it can still be heard.
|
|
72
|
+
*/
|
|
73
|
+
function closeSynchronously(close, file) {
|
|
74
|
+
if (typeof close !== 'function') return;
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = close();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// A synchronous throw is fine — already closed, or never opened. The delete still runs.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (result && typeof result.then === 'function') {
|
|
83
|
+
// Not swallowed: this is the only moment anybody could learn about it.
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.error(
|
|
86
|
+
`testkit: the close hook for ${path.basename(file)} returned a promise. An exit handler `
|
|
87
|
+
+ 'cannot await, so the database is still open when the file is deleted — on Windows that '
|
|
88
|
+
+ 'fails silently and the copy is left behind. Pass a SYNCHRONOUS close: '
|
|
89
|
+
+ 'the driver' + String.fromCharCode(39) + 's own db.close() usually is one, even when the wrapper is not.');
|
|
90
|
+
// ...and do not let it become an unhandled rejection on the way out.
|
|
91
|
+
result.then(undefined, () => {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
/**
|
|
58
96
|
* Copy `livePath` to a private temp file and return its path.
|
|
59
97
|
*
|
|
@@ -63,6 +101,8 @@ function removeAll(base) {
|
|
|
63
101
|
* dir where to put it; defaults to the OS temp directory
|
|
64
102
|
* prefix default 'app' — apps pass their own so two projects cannot collide
|
|
65
103
|
* cleanup default true; false leaves the copy behind for inspection after a failure
|
|
104
|
+
* allowMissing default false; true means an absent live database copies nothing rather
|
|
105
|
+
* than throwing, for a test that builds its schema from migrations
|
|
66
106
|
* close optional () => void, run before deleting. On Windows an open handle makes unlink
|
|
67
107
|
* fail with EBUSY, so without this the exit hook is a no-op for any test that does not
|
|
68
108
|
* close its database — which is most of them.
|
|
@@ -70,7 +110,17 @@ function removeAll(base) {
|
|
|
70
110
|
function copyDatabase(o = {}) {
|
|
71
111
|
const live = o.livePath;
|
|
72
112
|
if (!live) throw new Error('copyDatabase({ livePath }): the database to copy is required');
|
|
73
|
-
|
|
113
|
+
// STRICT BY DEFAULT. A test that quietly ran against an empty database when it meant to use real
|
|
114
|
+
// data is its own kind of wrong, and silent is the worst way for that to happen. But a suite that
|
|
115
|
+
// BUILDS its schema from migrations is legitimately fine without one, and the hand-rolled loop
|
|
116
|
+
// this replaced tolerated it — `if (exists) copy` simply copied nothing. On a fresh clone, or a CI
|
|
117
|
+
// box that has never booted the app, strictness alone turns those tests into a module-scope crash.
|
|
118
|
+
const missing = !fs.existsSync(live);
|
|
119
|
+
if (missing && !o.allowMissing) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`copyDatabase: there is no database at ${live}. Pass allowMissing:true if this test builds `
|
|
122
|
+
+ 'its own schema and does not need one.');
|
|
123
|
+
}
|
|
74
124
|
|
|
75
125
|
const label = String(o.name || 'test').replace(/[^a-z0-9-]+/gi, '-');
|
|
76
126
|
const copy = path.join(o.dir || os.tmpdir(),
|
|
@@ -80,9 +130,11 @@ function copyDatabase(o = {}) {
|
|
|
80
130
|
removeAll(copy);
|
|
81
131
|
|
|
82
132
|
// 2. ...then the live database and whichever sidecars it actually has.
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
133
|
+
if (!missing) {
|
|
134
|
+
for (const suffix of SUFFIXES) {
|
|
135
|
+
const src = live + suffix;
|
|
136
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, copy + suffix);
|
|
137
|
+
}
|
|
86
138
|
}
|
|
87
139
|
|
|
88
140
|
// 3. And take it away afterwards. Best effort by nature: a process that is killed runs no exit
|
|
@@ -91,9 +143,7 @@ function copyDatabase(o = {}) {
|
|
|
91
143
|
process.on('exit', () => {
|
|
92
144
|
// CLOSE BEFORE DELETING, where the caller can. Windows refuses to unlink an open file, and
|
|
93
145
|
// this handler runs with the database still open unless something closes it.
|
|
94
|
-
|
|
95
|
-
try { o.close(); } catch (e) { /* already closed, or never opened */ }
|
|
96
|
-
}
|
|
146
|
+
closeSynchronously(o.close, copy);
|
|
97
147
|
removeAll(copy);
|
|
98
148
|
});
|
|
99
149
|
}
|
|
@@ -118,9 +168,7 @@ function freshDatabase(o = {}) {
|
|
|
118
168
|
removeAll(file);
|
|
119
169
|
if (o.cleanup !== false) {
|
|
120
170
|
process.on('exit', () => {
|
|
121
|
-
|
|
122
|
-
try { o.close(); } catch (e) { /* already closed, or never opened */ }
|
|
123
|
-
}
|
|
171
|
+
closeSynchronously(o.close, file);
|
|
124
172
|
removeAll(file);
|
|
125
173
|
});
|
|
126
174
|
}
|