w-orm-lowdb 1.0.34 → 1.0.36
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/dist/w-orm-lowdb.umd.js +2 -2
- package/dist/w-orm-lowdb.umd.js.map +1 -1
- package/docs/WOrmLowdb.html +12 -10
- package/docs/WOrmLowdb.mjs.html +64 -54
- package/docs/index.html +1 -1
- package/package.json +2 -2
- package/src/WOrmLowdb.mjs +63 -53
- package/test/unit-write-pre-error.test.mjs +443 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import w from 'wsemi'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import WOrm from '../src/WOrmLowdb.mjs'
|
|
5
|
+
import { mkObserver } from './lib/mkObserver.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//寫入入口之前置處理與錯誤路徑(帳本R-WRITE-ERR-01, 排程第80項; 來源為第77項外審Argon F-03(a))
|
|
9
|
+
//寫入入口insert、insertBulk、save、del於try之外原有讀取呼叫端輸入之前置處理(讀option、iseobj/isearr判定、cloneDeep(data)),
|
|
10
|
+
//其拋錯者(原型為null或constructor為null之Date、存取器拋錯、Proxy trap拋錯、深度過大之stack overflow等)修前reject但不發error事件,
|
|
11
|
+
//規則: 任何因輸入導致之失敗皆走同一錯誤路徑, 即「error事件恰一次且先於reject, 不發change, 資料庫不變, 訊息不回顯值」
|
|
12
|
+
describe('write-pre-error', function() {
|
|
13
|
+
let url = './test/_tmp/write-pre-error'
|
|
14
|
+
|
|
15
|
+
//MARK, 放進紀錄內之標記字串, 錯誤訊息不得回顯資料值(帳本規則2)
|
|
16
|
+
let MARK = 'SECRET-MARK-9f3a'
|
|
17
|
+
|
|
18
|
+
//壞輸入之建構器: 皆使lodash cloneDeep或其前之iseobj/isearr同步拋錯
|
|
19
|
+
let nullProtoDate = () => {
|
|
20
|
+
let d = new Date(0)
|
|
21
|
+
Object.setPrototypeOf(d, null)
|
|
22
|
+
return d
|
|
23
|
+
}
|
|
24
|
+
let nullCtorDate = () => {
|
|
25
|
+
let d = new Date(0)
|
|
26
|
+
Object.defineProperty(d, 'constructor', { value: null })
|
|
27
|
+
return d
|
|
28
|
+
}
|
|
29
|
+
let deepObj = (n) => {
|
|
30
|
+
let o = {}
|
|
31
|
+
let cur = o
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
cur.n = {}
|
|
34
|
+
cur = cur.n
|
|
35
|
+
}
|
|
36
|
+
return o
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//throwingGetter, 讀取欄位x時拋出v(v可為任意型別之值)
|
|
40
|
+
let throwingGetter = (v) => {
|
|
41
|
+
return {
|
|
42
|
+
get x() {
|
|
43
|
+
throw v
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
let arr1Getter = () => {
|
|
48
|
+
let a = [0]
|
|
49
|
+
Object.defineProperty(a, '0', {
|
|
50
|
+
get() {
|
|
51
|
+
throw new Error('elem-getter-boom')
|
|
52
|
+
},
|
|
53
|
+
enumerable: true,
|
|
54
|
+
})
|
|
55
|
+
return a
|
|
56
|
+
}
|
|
57
|
+
let proxyOwnKeysThrows = () => {
|
|
58
|
+
return new Proxy({ id: 'z1' }, {
|
|
59
|
+
ownKeys() {
|
|
60
|
+
throw new Error('ownKeys-boom')
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
let proxyTagThrows = () => {
|
|
65
|
+
return new Proxy({ id: 'z1' }, {
|
|
66
|
+
get(t, k) {
|
|
67
|
+
if (k === Symbol.toStringTag) {
|
|
68
|
+
throw new Error('tag-boom')
|
|
69
|
+
}
|
|
70
|
+
return t[k]
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//optGetterThrows, option之key欄位讀取時拋錯
|
|
76
|
+
let optGetterThrows = (key) => {
|
|
77
|
+
let o = {}
|
|
78
|
+
Object.defineProperty(o, key, {
|
|
79
|
+
get() {
|
|
80
|
+
throw new Error('opt-boom')
|
|
81
|
+
},
|
|
82
|
+
enumerable: true,
|
|
83
|
+
})
|
|
84
|
+
return o
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//inputs, data型之壞輸入(每次呼叫重建, 以便驗證error事件之data引數為呼叫端傳入之同一物件)
|
|
88
|
+
let inputs = [
|
|
89
|
+
{ key: 'null-proto Date (field in batch)', mk: () => [{ id: 'z1', secret: MARK, t: nullProtoDate() }] },
|
|
90
|
+
{ key: 'null-proto Date (single object)', mk: () => ({ id: 'z1', secret: MARK, t: nullProtoDate() }) },
|
|
91
|
+
{ key: 'null-proto Date (3rd record of batch)', mk: () => [{ id: 'z1', secret: MARK }, { id: 'z2' }, { id: 'z3', t: nullProtoDate() }] },
|
|
92
|
+
{ key: 'Date with null constructor', mk: () => [{ id: 'z1', secret: MARK, t: nullCtorDate() }] },
|
|
93
|
+
{ key: 'throwing getter (nested field)', mk: () => [{ id: 'z1', secret: MARK, o: throwingGetter(new Error('getter-boom')) }] },
|
|
94
|
+
{ key: 'deep nesting (stack overflow)', mk: () => [{ id: 'z1', secret: MARK, o: deepObj(50000) }] },
|
|
95
|
+
{ key: 'Proxy whose ownKeys throws (iseobj)', mk: proxyOwnKeysThrows },
|
|
96
|
+
{ key: 'Proxy whose toStringTag read throws (iseobj)', mk: proxyTagThrows },
|
|
97
|
+
{ key: 'one-element array whose [0] getter throws (isearr)', mk: arr1Getter },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
//inputMsgs, 各壞輸入應原樣通過之訊息(帳本規則2: 訊息為拋出者之原訊息, 不被改寫為泛用訊息; 引擎/lodash之訊息以關鍵字比對, 不綁死版本)
|
|
101
|
+
let inputMsgs = {
|
|
102
|
+
'null-proto Date (field in batch)': /primitive/i,
|
|
103
|
+
'null-proto Date (single object)': /primitive/i,
|
|
104
|
+
'null-proto Date (3rd record of batch)': /primitive/i,
|
|
105
|
+
'Date with null constructor': /constructor/i,
|
|
106
|
+
'throwing getter (nested field)': 'getter-boom',
|
|
107
|
+
'deep nesting (stack overflow)': /call stack/i,
|
|
108
|
+
'Proxy whose ownKeys throws (iseobj)': 'ownKeys-boom',
|
|
109
|
+
'Proxy whose toStringTag read throws (iseobj)': 'tag-boom',
|
|
110
|
+
'one-element array whose [0] getter throws (isearr)': 'elem-getter-boom',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//thrownValues, 呼叫端存取器拋出之值之型別(帳本規則1: 含拋出值本身無法轉為字串者, error事件仍恰一次): reject之值為拋出者本身, 事件之訊息為字串(msg)
|
|
114
|
+
let thrownValues = [
|
|
115
|
+
{ key: 'Error', mk: () => new Error('plain-boom'), msg: 'plain-boom' },
|
|
116
|
+
{ key: 'string', mk: () => 'str-boom', msg: 'str-boom' },
|
|
117
|
+
{ key: 'number', mk: () => 42, msg: '42' },
|
|
118
|
+
{ key: 'null', mk: () => null, msg: 'null' },
|
|
119
|
+
{ key: 'undefined', mk: () => undefined, msg: 'undefined' },
|
|
120
|
+
{ key: 'null-proto object (not convertible to string)', mk: () => Object.create(null), msg: 'unknown error' },
|
|
121
|
+
{
|
|
122
|
+
key: 'object whose message getter throws',
|
|
123
|
+
mk: () => {
|
|
124
|
+
return {
|
|
125
|
+
get message() {
|
|
126
|
+
throw new Error('message-getter-boom')
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
msg: 'unknown error',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'object whose toString throws',
|
|
134
|
+
mk: () => {
|
|
135
|
+
return {
|
|
136
|
+
toString() {
|
|
137
|
+
throw new Error('toString-boom')
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
msg: 'unknown error',
|
|
142
|
+
},
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
//entries, 寫入入口(4格). key為資料庫資料夾用之唯一名稱, valid為可成功寫入(或刪除)之數據
|
|
146
|
+
let entries = [
|
|
147
|
+
{ key: 'insert', fn: 'insert', call: (wo, d, o) => wo.insert(d, o), early: () => ({ n: 0, nInserted: 0, ok: 1 }), valid: () => ({ id: 'valid-1' }) },
|
|
148
|
+
{ key: 'insertBulk', fn: 'insertBulk', call: (wo, d) => wo.insertBulk(d), early: () => ({ n: 0, nInserted: 0, ok: 1 }), valid: () => ({ id: 'valid-2' }) },
|
|
149
|
+
{ key: 'save', fn: 'save', call: (wo, d, o) => wo.save(d, o), early: () => [], valid: () => ({ id: 'valid-3' }) },
|
|
150
|
+
{ key: 'del', fn: 'del', call: (wo, d) => wo.del(d), early: () => [], valid: () => ({ id: 'seed1' }) },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
//optCases, option型之壞輸入(搭配合法data): 讀option亦為try之外之前置處理; sameRef: 事件之data是否為呼叫端傳入之同一物件(insert先讀option再複製故是, save先複製再讀option故為複本)
|
|
154
|
+
let optCases = [
|
|
155
|
+
{ fn: 'insert', key: 'option.returnList getter throws', mkOpt: () => optGetterThrows('returnList'), sameRef: true, msg: 'opt-boom' },
|
|
156
|
+
{ fn: 'save', key: 'option.autoInsert getter throws', mkOpt: () => optGetterThrows('autoInsert'), sameRef: false, msg: 'opt-boom' },
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
//mk: 於測試資料夾下建立資料庫檔(url為檔案路徑)
|
|
160
|
+
let mk = (name, opt = {}) => {
|
|
161
|
+
let file = `${url}/${name}.json`
|
|
162
|
+
w.fsDeleteFile(file)
|
|
163
|
+
return WOrm({ url: file, db: 'worm', cl: name, ...opt })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//snap, 資料庫現況(升冪)
|
|
167
|
+
let snap = async(wo) => (await wo.select()).sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
168
|
+
|
|
169
|
+
//seed, 寫入前之數據
|
|
170
|
+
let seed = () => [{ id: 'seed1', a: 1 }, { id: 'seed2', a: 2 }]
|
|
171
|
+
|
|
172
|
+
before(function() {
|
|
173
|
+
w.fsDeleteFolder(url)
|
|
174
|
+
fs.mkdirSync(url, { recursive: true })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
after(function() {
|
|
178
|
+
w.fsDeleteFolder(url)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
//chkEv, 錯誤路徑之共同斷言(帳本規則1、4): error事件於reject前恰一次(mode為該入口), 不發change; 回傳該error事件
|
|
182
|
+
let chkEv = (out, entry) => {
|
|
183
|
+
assert.strict.equal(out.ok, false, `${entry.fn} should reject`)
|
|
184
|
+
let evErr = out.evs.filter((v) => v.t === 'error')
|
|
185
|
+
let evChg = out.evs.filter((v) => v.t === 'change')
|
|
186
|
+
assert.strict.equal(evErr.length, 1, `${entry.fn} should emit error exactly once`)
|
|
187
|
+
assert.strict.equal(out.nAtSettle, 1, `${entry.fn} should emit error before reject`)
|
|
188
|
+
assert.strict.equal(evErr[0].settledAtEmit, false, `${entry.fn} should emit error before reject`)
|
|
189
|
+
assert.strict.equal(evErr[0].mode, entry.fn)
|
|
190
|
+
assert.strict.equal(evChg.length, 0, `${entry.fn} should not emit change`)
|
|
191
|
+
return evErr[0]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//chkData, 事件之data引數(帳本規則4): sameRef為真者須為呼叫端傳入之同一物件(拋錯於複本建立之前); 為假者為複本, 以深度相等比對
|
|
195
|
+
let chkData = (ev, d, sameRef, entry) => {
|
|
196
|
+
if (sameRef) {
|
|
197
|
+
assert.strict.ok(ev.data === d, `${entry.fn} error event data should be the caller's input (no copy exists yet)`)
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
assert.strict.ok(ev.data !== d, `${entry.fn} error event data should be the copy (it exists already), not the caller's object`)
|
|
201
|
+
assert.deepStrictEqual(ev.data, d, `${entry.fn} error event data should equal the caller's input`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//chkRej, 拋出者為Error之共同斷言(帳本規則1、2、4): reject為Error, 事件之訊息同reject之message, 事件之data為呼叫端傳入之原值, 訊息不回顯資料值
|
|
206
|
+
let chkRej = (out, entry, d, sameRef, msg) => {
|
|
207
|
+
let ev = chkEv(out, entry)
|
|
208
|
+
assert.strict.ok(out.err instanceof Error, `${entry.fn} should reject with Error`)
|
|
209
|
+
assert.strict.equal(ev.err, out.err.message, `${entry.fn} error event should carry the reject message`)
|
|
210
|
+
//帳本規則2: 訊息為拋出者之原訊息(不被改寫為泛用訊息)
|
|
211
|
+
assert.strict.ok(msg instanceof RegExp ? msg.test(out.err.message) : out.err.message === msg, `${entry.fn} message should be the thrower's own message, got "${out.err.message}"`)
|
|
212
|
+
chkData(ev, d, sameRef, entry)
|
|
213
|
+
assert.strict.ok(!String(out.err.message).includes(MARK), `${entry.fn} message should not echo data values`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (let entry of entries) {
|
|
217
|
+
describe(entry.key, function() {
|
|
218
|
+
let wo = null
|
|
219
|
+
let ob = null
|
|
220
|
+
let nNext = 0
|
|
221
|
+
|
|
222
|
+
before(async function() {
|
|
223
|
+
wo = mk(entry.key)
|
|
224
|
+
ob = mkObserver(wo)
|
|
225
|
+
await wo.insertBulk(seed())
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
//chkAfter, 失敗後資料庫逐筆不變, 且其後之寫入照常(佇列未被卡住)
|
|
229
|
+
let chkAfter = async(snapBefore) => {
|
|
230
|
+
assert.deepStrictEqual(await snap(wo), snapBefore, `${entry.fn} database should be unchanged`)
|
|
231
|
+
let id = `next-${nNext++}`
|
|
232
|
+
let r = await wo.insert({ id })
|
|
233
|
+
assert.strict.equal(r.nInserted, 1, `${entry.fn} following insert should work`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (let input of inputs) {
|
|
237
|
+
it(`${input.key}: rejects with one error event before reject, no change, database unchanged`, async function() {
|
|
238
|
+
//對應建議書驗證建議1: 修前紅為無error事件
|
|
239
|
+
let snapBefore = await snap(wo)
|
|
240
|
+
let d = input.mk()
|
|
241
|
+
let out = await ob.run(() => entry.call(wo, d))
|
|
242
|
+
chkRej(out, entry, d, true, inputMsgs[input.key])
|
|
243
|
+
await chkAfter(snapBefore)
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (let oc of optCases.filter((v) => v.fn === entry.fn)) {
|
|
248
|
+
it(`${oc.key}: rejects with one error event before reject, no change, database unchanged`, async function() {
|
|
249
|
+
let snapBefore = await snap(wo)
|
|
250
|
+
let d = [{ id: 'o1', secret: MARK, a: 1 }]
|
|
251
|
+
let out = await ob.run(() => entry.call(wo, d, oc.mkOpt()))
|
|
252
|
+
chkRej(out, entry, d, oc.sameRef, oc.msg)
|
|
253
|
+
await chkAfter(snapBefore)
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let tv of thrownValues) {
|
|
258
|
+
it(`thrown value (${tv.key}): rejects with the thrown value, one error event with string message "${tv.msg}", no change`, async function() {
|
|
259
|
+
//帳本規則1: 拋出值本身無法轉為字串者(null原型物件、message存取器拋錯、toString拋錯)亦須恰發出一次error事件(外審agy-opus F-01), reject之值為拋出者本身
|
|
260
|
+
let snapBefore = await snap(wo)
|
|
261
|
+
let thrown = tv.mk()
|
|
262
|
+
let d = [{ id: 'z1', secret: MARK, o: throwingGetter(thrown) }]
|
|
263
|
+
let out = await ob.run(() => entry.call(wo, d))
|
|
264
|
+
let ev = chkEv(out, entry)
|
|
265
|
+
assert.strict.ok(Object.is(out.err, thrown), `${entry.fn} should reject with the thrown value itself`)
|
|
266
|
+
assert.strict.equal(typeof ev.err, 'string', `${entry.fn} error event message should be a string`)
|
|
267
|
+
assert.strict.equal(ev.err, tv.msg, `${entry.fn} error event message`)
|
|
268
|
+
assert.strict.ok(!ev.err.includes(MARK), `${entry.fn} message should not echo data values`)
|
|
269
|
+
chkData(ev, d, true, entry)
|
|
270
|
+
await chkAfter(snapBefore)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (entry.fn === 'insert') {
|
|
275
|
+
it('bad data with bad option: reports the option error (option is read before the data is copied)', async function() {
|
|
276
|
+
//insert讀option先於複製data(與修前之讀取順序相同), 兩者皆壞時回報option之錯
|
|
277
|
+
let snapBefore = await snap(wo)
|
|
278
|
+
let d = inputs[0].mk()
|
|
279
|
+
let out = await ob.run(() => wo.insert(d, optGetterThrows('returnList')))
|
|
280
|
+
chkRej(out, entry, d, true, 'opt-boom')
|
|
281
|
+
await chkAfter(snapBefore)
|
|
282
|
+
})
|
|
283
|
+
it('invalid data with bad option: still rejects with the option error (option is read before the validity check)', async function() {
|
|
284
|
+
let snapBefore = await snap(wo)
|
|
285
|
+
let out = await ob.run(() => wo.insert(null, optGetterThrows('returnList')))
|
|
286
|
+
chkRej(out, entry, null, true, 'opt-boom')
|
|
287
|
+
await chkAfter(snapBefore)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (entry.fn === 'save') {
|
|
292
|
+
it('bad data with bad option: reports the data error (data is copied before the option is read)', async function() {
|
|
293
|
+
//save先複製data再讀option(與修前相同), 兩者皆壞時回報data之錯
|
|
294
|
+
let snapBefore = await snap(wo)
|
|
295
|
+
let d = inputs[0].mk()
|
|
296
|
+
let out = await ob.run(() => wo.save(d, optGetterThrows('autoInsert')))
|
|
297
|
+
chkRej(out, entry, d, true, /primitive/i)
|
|
298
|
+
assert.strict.notEqual(out.err.message, 'opt-boom')
|
|
299
|
+
await chkAfter(snapBefore)
|
|
300
|
+
})
|
|
301
|
+
it('invalid data with bad option: returns [] without reading the option (control, unchanged)', async function() {
|
|
302
|
+
let snapBefore = await snap(wo)
|
|
303
|
+
let out = await ob.run(() => wo.save(null, optGetterThrows('autoInsert')))
|
|
304
|
+
assert.strict.equal(out.ok, true)
|
|
305
|
+
assert.deepStrictEqual(out.val, [])
|
|
306
|
+
assert.strict.equal(out.evs.length, 0)
|
|
307
|
+
await chkAfter(snapBefore)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
//對照組(修前即綠): 帳本規則3, 輸入無效之早退結果不變, 不發任何事件
|
|
314
|
+
describe('control: invalid input returns early (unchanged)', function() {
|
|
315
|
+
let invalids = [
|
|
316
|
+
{ key: 'null', v: null },
|
|
317
|
+
{ key: 'undefined', v: undefined },
|
|
318
|
+
{ key: 'string', v: 'str' },
|
|
319
|
+
{ key: 'number', v: 123 },
|
|
320
|
+
{ key: 'empty array', v: [] },
|
|
321
|
+
{ key: 'empty object', v: {} },
|
|
322
|
+
{ key: 'null-proto Date as data (not an object/array, so invalid input)', v: nullProtoDate() },
|
|
323
|
+
]
|
|
324
|
+
for (let entry of entries) {
|
|
325
|
+
describe(entry.fn, function() {
|
|
326
|
+
let wo = null
|
|
327
|
+
let ob = null
|
|
328
|
+
before(async function() {
|
|
329
|
+
wo = mk(`early-${entry.key}`)
|
|
330
|
+
ob = mkObserver(wo)
|
|
331
|
+
await wo.insertBulk(seed())
|
|
332
|
+
})
|
|
333
|
+
for (let inv of invalids) {
|
|
334
|
+
it(`${inv.key}: resolves ${JSON.stringify(entry.early())}, no events, database unchanged`, async function() {
|
|
335
|
+
let snapBefore = await snap(wo)
|
|
336
|
+
let out = await ob.run(() => entry.call(wo, inv.v))
|
|
337
|
+
assert.strict.equal(out.ok, true)
|
|
338
|
+
assert.deepStrictEqual(out.val, entry.early())
|
|
339
|
+
assert.strict.equal(out.evs.length, 0)
|
|
340
|
+
assert.deepStrictEqual(await snap(wo), snapBefore)
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
describe('insert with returnList:true', function() {
|
|
346
|
+
let wo = null
|
|
347
|
+
let ob = null
|
|
348
|
+
before(function() {
|
|
349
|
+
wo = mk('early-insert-list')
|
|
350
|
+
ob = mkObserver(wo)
|
|
351
|
+
})
|
|
352
|
+
for (let inv of invalids) {
|
|
353
|
+
it(`${inv.key}: resolves [], no events`, async function() {
|
|
354
|
+
let out = await ob.run(() => wo.insert(inv.v, { returnList: true }))
|
|
355
|
+
assert.strict.equal(out.ok, true)
|
|
356
|
+
assert.deepStrictEqual(out.val, [])
|
|
357
|
+
assert.strict.equal(out.evs.length, 0)
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
//對照組(修前即綠): 一般輸入之行為不變, error為0次, change為各入口原有之事件
|
|
364
|
+
describe('control: normal input unchanged', function() {
|
|
365
|
+
let cases = [
|
|
366
|
+
{ fn: 'insert', call: (wo) => wo.insert({ id: 'n1', a: 1 }), val: { n: 1, nInserted: 1, ok: 1 }, changes: ['insert'] },
|
|
367
|
+
{ fn: 'insertBulk', call: (wo) => wo.insertBulk([{ id: 'n1', a: 1 }, { id: 'n2', a: 2 }]), val: { n: 2, nInserted: 2, ok: 1 }, changes: ['insertBulk'] },
|
|
368
|
+
{ fn: 'save', call: (wo) => wo.save({ id: 'n1', a: 1 }), val: [{ n: 1, nInserted: 1, nModified: 0, ok: 1 }], changes: ['insert', 'save'] },
|
|
369
|
+
{ fn: 'del', call: (wo) => wo.del({ id: 'seed1' }), val: [{ n: 1, nDeleted: 1, ok: 1 }], changes: ['del'] },
|
|
370
|
+
]
|
|
371
|
+
for (let c of cases) {
|
|
372
|
+
it(`${c.fn}: resolves, no error event, change events ${JSON.stringify(c.changes)}`, async function() {
|
|
373
|
+
let wo = mk(`normal-${c.fn}`)
|
|
374
|
+
let ob = mkObserver(wo)
|
|
375
|
+
await wo.insertBulk(seed())
|
|
376
|
+
let out = await ob.run(() => c.call(wo))
|
|
377
|
+
assert.strict.equal(out.ok, true)
|
|
378
|
+
assert.deepStrictEqual(out.val, c.val)
|
|
379
|
+
assert.strict.equal(out.evs.filter((v) => v.t === 'error').length, 0)
|
|
380
|
+
assert.deepStrictEqual(out.evs.filter((v) => v.t === 'change').map((v) => v.mode), c.changes)
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
//useCache之快取(帳本規則3、5): 前置處理失敗與其他整批性錯誤一致須重設快取, 輸入無效之早退不重設
|
|
386
|
+
//觀測法: 快取為熱時自實例外改寫資料庫檔(內容換成ext1), 其後select若讀到檔案內容(含ext1、不含seed)即快取已重設, 讀到舊內容(seed1、seed2)即快取仍在
|
|
387
|
+
describe('useCache: a failed write resets the cache, an early return keeps it (external review agy-opus F-02)', function() {
|
|
388
|
+
for (let entry of entries) {
|
|
389
|
+
describe(entry.fn, function() {
|
|
390
|
+
//arm, 建立使用快取之實例, 令快取為熱(內容為seed), 再自外部改寫檔案為ext1
|
|
391
|
+
let arm = async(tag) => {
|
|
392
|
+
let name = `cache-reset-${entry.key}-${tag}`
|
|
393
|
+
let wo = mk(name, { useCache: true })
|
|
394
|
+
await wo.insertBulk(seed())
|
|
395
|
+
await wo.select()
|
|
396
|
+
fs.writeFileSync(`${url}/${name}.json`, JSON.stringify({ [`worm:${name}`]: [{ id: 'ext1' }] }))
|
|
397
|
+
return wo
|
|
398
|
+
}
|
|
399
|
+
let ids = async(wo) => (await wo.select()).map((v) => v.id).sort()
|
|
400
|
+
|
|
401
|
+
it('control: without any write the cache still serves the old content', async function() {
|
|
402
|
+
let wo = await arm('ctl')
|
|
403
|
+
assert.deepStrictEqual(await ids(wo), ['seed1', 'seed2'])
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('control: a successful write resets the cache (next select reads the file)', async function() {
|
|
407
|
+
let wo = await arm('ok')
|
|
408
|
+
await entry.call(wo, entry.valid())
|
|
409
|
+
let r = await ids(wo)
|
|
410
|
+
assert.strict.ok(r.includes('ext1') && !r.includes('seed1'), `select should read the file, got ${JSON.stringify(r)}`)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('a write rejected by bad input resets the cache (next select reads the file)', async function() {
|
|
414
|
+
let wo = await arm('bad')
|
|
415
|
+
await entry.call(wo, inputs[0].mk()).then(() => assert.fail('should reject'), () => {})
|
|
416
|
+
let r = await ids(wo)
|
|
417
|
+
assert.strict.ok(r.includes('ext1') && !r.includes('seed1'), `select should read the file, got ${JSON.stringify(r)}`)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('an early return (invalid input) keeps the cache (next select still serves the old content)', async function() {
|
|
421
|
+
let wo = await arm('early')
|
|
422
|
+
await entry.call(wo, null)
|
|
423
|
+
assert.deepStrictEqual(await ids(wo), ['seed1', 'seed2'])
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
//對照組(修前即綠): useCache熱快取後前置處理失敗, 再select與先前一致(失敗不使快取失真)
|
|
430
|
+
describe('control: useCache stays coherent after a failed write', function() {
|
|
431
|
+
for (let entry of entries) {
|
|
432
|
+
it(`${entry.fn}: select after failure equals select before`, async function() {
|
|
433
|
+
let wo = mk(`cache-${entry.key}`, { useCache: true })
|
|
434
|
+
await wo.insertBulk(seed())
|
|
435
|
+
let before = await snap(wo) //填入快取
|
|
436
|
+
let d = inputs[0].mk()
|
|
437
|
+
await entry.call(wo, d).then(() => assert.fail('should reject'), () => {})
|
|
438
|
+
assert.deepStrictEqual(await snap(wo), before)
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
})
|