@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.2.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
- if (!fs.existsSync(live)) throw new Error(`copyDatabase: there is no database at ${live}`);
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
- for (const suffix of SUFFIXES) {
84
- const src = live + suffix;
85
- if (fs.existsSync(src)) fs.copyFileSync(src, copy + suffix);
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
- if (typeof o.close === 'function') {
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
- if (typeof o.close === 'function') {
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
  }