1081 lines
33 KiB
JavaScript
1081 lines
33 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const test = require('tape')
|
||
|
const sinon = require('sinon')
|
||
|
const isBuffer = require('is-buffer')
|
||
|
const { Buffer } = require('buffer')
|
||
|
const { AbstractLevel, AbstractChainedBatch } = require('..')
|
||
|
const { MinimalLevel } = require('./util')
|
||
|
const getRangeOptions = require('../lib/range-options')
|
||
|
|
||
|
const testCommon = require('./common')({
|
||
|
test,
|
||
|
factory () {
|
||
|
return new AbstractLevel({ encodings: { utf8: true } })
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const rangeOptions = ['gt', 'gte', 'lt', 'lte']
|
||
|
|
||
|
function implement (ctor, methods) {
|
||
|
class Test extends ctor {}
|
||
|
|
||
|
for (const k in methods) {
|
||
|
Test.prototype[k] = methods[k]
|
||
|
}
|
||
|
|
||
|
return Test
|
||
|
}
|
||
|
|
||
|
// Temporary test for browsers
|
||
|
// Not supported on Safari < 12 and Safari iOS < 12
|
||
|
test('async generator', async function (t) {
|
||
|
let ended = false
|
||
|
|
||
|
const end = async () => {
|
||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||
|
ended = true
|
||
|
}
|
||
|
|
||
|
async function * generator () {
|
||
|
try {
|
||
|
yield 1
|
||
|
yield 2
|
||
|
yield 3
|
||
|
yield 4
|
||
|
} finally {
|
||
|
// Test that we're always able to cleanup resources
|
||
|
await end()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const res = []
|
||
|
|
||
|
for await (const x of generator()) {
|
||
|
res.push(x)
|
||
|
if (x === 2) break
|
||
|
}
|
||
|
|
||
|
t.same(res, [1, 2])
|
||
|
t.is(ended, true)
|
||
|
|
||
|
ended = false
|
||
|
|
||
|
try {
|
||
|
for await (const x of generator()) {
|
||
|
res.push(x)
|
||
|
if (x === 2) throw new Error('userland error')
|
||
|
}
|
||
|
} catch (err) {
|
||
|
t.is(err.message, 'userland error')
|
||
|
}
|
||
|
|
||
|
t.same(res, [1, 2, 1, 2])
|
||
|
t.is(ended, true)
|
||
|
})
|
||
|
|
||
|
/**
|
||
|
* Extensibility
|
||
|
*/
|
||
|
|
||
|
test('test core extensibility', function (t) {
|
||
|
const Test = implement(AbstractLevel)
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
t.equal(test.status, 'opening', 'status is opening')
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
test('test open() extensibility when new', function (t) {
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { createIfMissing: true, errorIfExists: false }
|
||
|
const Test = implement(AbstractLevel, { _open: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
const test2 = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.open(expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _open() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _open() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 2, 'got two arguments')
|
||
|
t.deepEqual(spy.getCall(0).args[0], expectedOptions, 'got default options argument')
|
||
|
|
||
|
test2.open({ options: 1 }, expectedCb)
|
||
|
|
||
|
expectedOptions.options = 1
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _open() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test2, '`this` on _open() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 2, 'got two arguments')
|
||
|
t.deepEqual(spy.getCall(1).args[0], expectedOptions, 'got expected options argument')
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
test('test open() extensibility when open', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy(function (options, cb) { this.nextTick(cb) })
|
||
|
const Test = implement(AbstractLevel, { _open: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
t.is(spy.callCount, 1, 'got _open() call')
|
||
|
|
||
|
test.open(t.ifError.bind(t))
|
||
|
|
||
|
t.is(spy.callCount, 1, 'did not get second _open() call')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test opening explicitly gives a chance to capture an error', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy(function (options, cb) { this.nextTick(cb, new Error('_open error')) })
|
||
|
const Test = implement(AbstractLevel, { _open: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.open(function (err) {
|
||
|
t.is(spy.callCount, 1, 'got _open() call')
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err && err.cause && err.cause.message, '_open error')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test opening explicitly gives a chance to capture an error with promise', async function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy(function (options, cb) { this.nextTick(cb, new Error('_open error')) })
|
||
|
const Test = implement(AbstractLevel, { _open: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
try {
|
||
|
await test.open()
|
||
|
} catch (err) {
|
||
|
t.is(spy.callCount, 1, 'got _open() call')
|
||
|
t.is(err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err.cause && err.cause.message, '_open error')
|
||
|
}
|
||
|
})
|
||
|
|
||
|
test('test constructor options are forwarded to open()', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy(function (options, cb) { this.nextTick(cb) })
|
||
|
const Test = implement(AbstractLevel, { _open: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } }, {
|
||
|
passive: true,
|
||
|
keyEncoding: 'json',
|
||
|
valueEncoding: 'json',
|
||
|
createIfMissing: false,
|
||
|
foo: 123
|
||
|
})
|
||
|
|
||
|
test.open(function (err) {
|
||
|
t.ifError(err, 'no open() error')
|
||
|
t.is(spy.callCount, 1, 'got _open() call')
|
||
|
t.same(spy.getCall(0).args[0], {
|
||
|
foo: 123,
|
||
|
createIfMissing: false,
|
||
|
errorIfExists: false
|
||
|
}, 'does not forward passive, keyEncoding and valueEncoding options')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test close() extensibility when open', function (t) {
|
||
|
t.plan(4)
|
||
|
|
||
|
const spy = sinon.spy(function (cb) { this.nextTick(cb) })
|
||
|
const Test = implement(AbstractLevel, { _close: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.close(function (err) {
|
||
|
t.ifError(err, 'no close() error')
|
||
|
t.is(spy.callCount, 1, 'got _close() call')
|
||
|
t.is(spy.getCall(0).thisValue, test, '`this` on _close() was correct')
|
||
|
t.is(spy.getCall(0).args.length, 1, 'got one arguments')
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test close() extensibility when new', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy(function (cb) { this.nextTick(cb) })
|
||
|
const Test = implement(AbstractLevel, { _close: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.close(function (err) {
|
||
|
t.ifError(err, 'no close() error')
|
||
|
t.is(spy.callCount, 1, 'did get _close() call')
|
||
|
})
|
||
|
|
||
|
t.is(spy.callCount, 0, 'did not get _close() call')
|
||
|
})
|
||
|
|
||
|
test('test open(), close(), open() with twice failed open', function (t) {
|
||
|
t.plan(8)
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
const order = []
|
||
|
|
||
|
let opens = 0
|
||
|
|
||
|
db.on('open', t.fail.bind(t))
|
||
|
db.on('closed', t.fail.bind(t))
|
||
|
|
||
|
db._open = function (options, callback) {
|
||
|
t.pass('called')
|
||
|
this.nextTick(callback, new Error('test' + (++opens)))
|
||
|
}
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err && err.cause && err.cause.message, 'test1')
|
||
|
order.push('A')
|
||
|
})
|
||
|
|
||
|
db.close(function (err) {
|
||
|
t.ifError(err)
|
||
|
order.push('B')
|
||
|
})
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err && err.cause && err.cause.message, 'test2')
|
||
|
order.push('C')
|
||
|
t.same(order, ['A', 'B', 'C'], 'order is ok')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test open(), close(), open() with first failed open', function (t) {
|
||
|
t.plan(9)
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
const order = []
|
||
|
|
||
|
let opens = 0
|
||
|
|
||
|
db.on('open', () => { order.push('open event') })
|
||
|
db.on('closed', t.fail.bind(t))
|
||
|
|
||
|
db._open = function (options, callback) {
|
||
|
t.pass('called')
|
||
|
this.nextTick(callback, opens++ ? null : new Error('test'))
|
||
|
}
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.ifError(err)
|
||
|
t.is(db.status, 'open')
|
||
|
order.push('A')
|
||
|
})
|
||
|
|
||
|
db.close(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_CLOSED')
|
||
|
t.is(db.status, 'open')
|
||
|
order.push('B')
|
||
|
})
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.ifError(err)
|
||
|
t.is(db.status, 'open')
|
||
|
order.push('C')
|
||
|
t.same(order, ['A', 'B', 'open event', 'C'], 'order is ok')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test open(), close(), open() with second failed open', function (t) {
|
||
|
t.plan(10)
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
const order = []
|
||
|
|
||
|
let opens = 0
|
||
|
|
||
|
db.on('open', t.fail.bind(t))
|
||
|
db.on('closed', t.fail.bind(t))
|
||
|
|
||
|
db._open = function (options, callback) {
|
||
|
t.pass('called')
|
||
|
this.nextTick(callback, opens++ ? new Error('test') : null)
|
||
|
}
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(db.status, 'closed')
|
||
|
order.push('A')
|
||
|
})
|
||
|
|
||
|
db.close(function (err) {
|
||
|
t.ifError(err)
|
||
|
t.is(db.status, 'closed')
|
||
|
order.push('B')
|
||
|
})
|
||
|
|
||
|
db.open(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err && err.cause.message, 'test')
|
||
|
t.is(db.status, 'closed')
|
||
|
order.push('C')
|
||
|
t.same(order, ['A', 'B', 'C'], 'order is ok')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test get() extensibility', function (t) {
|
||
|
t.plan(12)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { keyEncoding: 'utf8', valueEncoding: 'utf8' }
|
||
|
const expectedKey = 'a key'
|
||
|
const Test = implement(AbstractLevel, { _get: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } }, { keyEncoding: 'utf8' })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.get(expectedKey, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _get() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _get() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(0).args[0], expectedKey, 'got expected key argument')
|
||
|
t.deepEqual(spy.getCall(0).args[1], expectedOptions, 'got default options argument')
|
||
|
t.is(typeof spy.getCall(0).args[2], 'function', 'got cb argument')
|
||
|
|
||
|
test.get(expectedKey, { options: 1 }, expectedCb)
|
||
|
|
||
|
expectedOptions.options = 1
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _get() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _get() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(1).args[0], expectedKey, 'got expected key argument')
|
||
|
t.deepEqual(spy.getCall(1).args[1], expectedOptions, 'got expected options argument')
|
||
|
t.is(typeof spy.getCall(1).args[2], 'function', 'got cb argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test getMany() extensibility', function (t) {
|
||
|
t.plan(12)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { keyEncoding: 'utf8', valueEncoding: 'utf8' }
|
||
|
const expectedKey = 'a key'
|
||
|
const Test = implement(AbstractLevel, { _getMany: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.getMany([expectedKey], expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _getMany() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _getMany() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got three arguments')
|
||
|
t.deepEqual(spy.getCall(0).args[0], [expectedKey], 'got expected keys argument')
|
||
|
t.deepEqual(spy.getCall(0).args[1], expectedOptions, 'got default options argument')
|
||
|
t.is(typeof spy.getCall(0).args[2], 'function', 'got cb argument')
|
||
|
|
||
|
test.getMany([expectedKey], { options: 1 }, expectedCb)
|
||
|
|
||
|
expectedOptions.options = 1
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _getMany() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _getMany() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 3, 'got three arguments')
|
||
|
t.deepEqual(spy.getCall(1).args[0], [expectedKey], 'got expected key argument')
|
||
|
t.deepEqual(spy.getCall(1).args[1], expectedOptions, 'got expected options argument')
|
||
|
t.is(typeof spy.getCall(1).args[2], 'function', 'got cb argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test del() extensibility', function (t) {
|
||
|
t.plan(12)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { options: 1, keyEncoding: 'utf8' }
|
||
|
const expectedKey = 'a key'
|
||
|
const Test = implement(AbstractLevel, { _del: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.del(expectedKey, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _del() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _del() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(0).args[0], expectedKey, 'got expected key argument')
|
||
|
t.deepEqual(spy.getCall(0).args[1], { keyEncoding: 'utf8' }, 'got blank options argument')
|
||
|
t.equal(typeof spy.getCall(0).args[2], 'function', 'got cb argument')
|
||
|
|
||
|
test.del(expectedKey, expectedOptions, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _del() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _del() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(1).args[0], expectedKey, 'got expected key argument')
|
||
|
t.deepEqual(spy.getCall(1).args[1], expectedOptions, 'got expected options argument')
|
||
|
t.equal(typeof spy.getCall(1).args[2], 'function', 'got cb argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test put() extensibility', function (t) {
|
||
|
t.plan(14)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { options: 1, keyEncoding: 'utf8', valueEncoding: 'utf8' }
|
||
|
const expectedKey = 'a key'
|
||
|
const expectedValue = 'a value'
|
||
|
const Test = implement(AbstractLevel, { _put: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.put(expectedKey, expectedValue, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _put() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _put() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 4, 'got four arguments')
|
||
|
t.equal(spy.getCall(0).args[0], expectedKey, 'got expected key argument')
|
||
|
t.equal(spy.getCall(0).args[1], expectedValue, 'got expected value argument')
|
||
|
t.deepEqual(spy.getCall(0).args[2], { keyEncoding: 'utf8', valueEncoding: 'utf8' }, 'got default options argument')
|
||
|
t.equal(typeof spy.getCall(0).args[3], 'function', 'got cb argument')
|
||
|
|
||
|
test.put(expectedKey, expectedValue, expectedOptions, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _put() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _put() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 4, 'got four arguments')
|
||
|
t.equal(spy.getCall(1).args[0], expectedKey, 'got expected key argument')
|
||
|
t.equal(spy.getCall(1).args[1], expectedValue, 'got expected value argument')
|
||
|
t.deepEqual(spy.getCall(1).args[2], expectedOptions, 'got expected options argument')
|
||
|
t.equal(typeof spy.getCall(1).args[3], 'function', 'got cb argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test batch([]) (array-form) extensibility', function (t) {
|
||
|
t.plan(18)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { options: 1 }
|
||
|
const expectedArray = [
|
||
|
{ type: 'put', key: '1', value: '1', keyEncoding: 'utf8', valueEncoding: 'utf8' },
|
||
|
{ type: 'del', key: '2', keyEncoding: 'utf8' }
|
||
|
]
|
||
|
const Test = implement(AbstractLevel, { _batch: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.batch(expectedArray, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _batch() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _batch() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got three arguments')
|
||
|
t.deepEqual(spy.getCall(0).args[0], expectedArray, 'got expected array argument')
|
||
|
t.deepEqual(spy.getCall(0).args[1], {}, 'got expected options argument')
|
||
|
t.equal(typeof spy.getCall(0).args[2], 'function', 'got callback argument')
|
||
|
|
||
|
test.batch(expectedArray, expectedOptions, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _batch() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _batch() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 3, 'got three arguments')
|
||
|
t.deepEqual(spy.getCall(1).args[0], expectedArray, 'got expected array argument')
|
||
|
t.deepEqual(spy.getCall(1).args[1], expectedOptions, 'got expected options argument')
|
||
|
t.equal(typeof spy.getCall(1).args[2], 'function', 'got callback argument')
|
||
|
|
||
|
test.batch(expectedArray, null, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 3, 'got _batch() call')
|
||
|
t.equal(spy.getCall(2).thisValue, test, '`this` on _batch() was correct')
|
||
|
t.equal(spy.getCall(2).args.length, 3, 'got three arguments')
|
||
|
t.deepEqual(spy.getCall(2).args[0], expectedArray, 'got expected array argument')
|
||
|
t.ok(spy.getCall(2).args[1], 'options should not be null')
|
||
|
t.equal(typeof spy.getCall(2).args[2], 'function', 'got callback argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test batch([]) (array-form) with empty array is asynchronous', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const Test = implement(AbstractLevel, { _batch: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
let async = false
|
||
|
|
||
|
test.batch([], function (err) {
|
||
|
t.ifError(err, 'no error')
|
||
|
t.ok(async, 'callback is asynchronous')
|
||
|
|
||
|
// Assert that asynchronicity is provided by batch() rather than _batch()
|
||
|
t.is(spy.callCount, 0, '_batch() call was bypassed')
|
||
|
})
|
||
|
|
||
|
async = true
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test chained batch() extensibility', function (t) {
|
||
|
t.plan(16)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedCb = function () {}
|
||
|
const expectedOptions = { options: 1 }
|
||
|
const Test = implement(AbstractLevel, { _batch: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.batch().put('foo', 'bar').del('bang').write(expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _batch() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _batch() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(0).args[0].length, 2, 'got expected array argument')
|
||
|
t.deepEqual(spy.getCall(0).args[0][0], { keyEncoding: 'utf8', valueEncoding: 'utf8', type: 'put', key: 'foo', value: 'bar' }, 'got expected array argument[0]')
|
||
|
t.deepEqual(spy.getCall(0).args[0][1], { keyEncoding: 'utf8', type: 'del', key: 'bang' }, 'got expected array argument[1]')
|
||
|
t.deepEqual(spy.getCall(0).args[1], {}, 'got expected options argument')
|
||
|
t.is(typeof spy.getCall(0).args[2], 'function', 'got callback argument')
|
||
|
|
||
|
test.batch().put('foo', 'bar', expectedOptions).del('bang', expectedOptions).write(expectedOptions, expectedCb)
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _batch() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _batch() was correct')
|
||
|
t.equal(spy.getCall(1).args.length, 3, 'got three arguments')
|
||
|
t.equal(spy.getCall(1).args[0].length, 2, 'got expected array argument')
|
||
|
t.deepEqual(spy.getCall(1).args[0][0], { keyEncoding: 'utf8', valueEncoding: 'utf8', type: 'put', key: 'foo', value: 'bar', options: 1 }, 'got expected array argument[0]')
|
||
|
t.deepEqual(spy.getCall(1).args[0][1], { keyEncoding: 'utf8', type: 'del', key: 'bang', options: 1 }, 'got expected array argument[1]')
|
||
|
t.deepEqual(spy.getCall(1).args[1], { options: 1 }, 'got expected options argument')
|
||
|
t.is(typeof spy.getCall(1).args[2], 'function', 'got callback argument')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test chained batch() with no operations is asynchronous', function (t) {
|
||
|
t.plan(2)
|
||
|
|
||
|
const Test = implement(AbstractLevel, {})
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
let async = false
|
||
|
|
||
|
test.batch().write(function (err) {
|
||
|
t.ifError(err, 'no error')
|
||
|
t.ok(async, 'callback is asynchronous')
|
||
|
})
|
||
|
|
||
|
async = true
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test chained batch() (custom _chainedBatch) extensibility', function (t) {
|
||
|
t.plan(4)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const Test = implement(AbstractLevel, { _chainedBatch: spy })
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
test.batch()
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _chainedBatch() call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _chainedBatch() was correct')
|
||
|
|
||
|
test.batch()
|
||
|
|
||
|
t.equal(spy.callCount, 2, 'got _chainedBatch() call')
|
||
|
t.equal(spy.getCall(1).thisValue, test, '`this` on _chainedBatch() was correct')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch extensibility', function (t) {
|
||
|
const Batch = implement(AbstractChainedBatch)
|
||
|
const db = testCommon.factory()
|
||
|
const test = new Batch(db)
|
||
|
t.ok(test.db === db, 'instance has db reference')
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch expects a db', function (t) {
|
||
|
t.plan(1)
|
||
|
|
||
|
const Test = implement(AbstractChainedBatch)
|
||
|
|
||
|
try {
|
||
|
// eslint-disable-next-line no-new
|
||
|
new Test()
|
||
|
} catch (err) {
|
||
|
t.is(err.message, 'The first argument must be an abstract-level database, received undefined')
|
||
|
}
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#write() extensibility', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
let batch
|
||
|
|
||
|
const Test = implement(AbstractChainedBatch, {
|
||
|
_write: function (options, callback) {
|
||
|
t.same(options, {})
|
||
|
t.is(this, batch, 'thisArg on _write() is correct')
|
||
|
this.nextTick(callback)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
batch = new Test(db)
|
||
|
|
||
|
// Without any operations, _write isn't called
|
||
|
batch.put('foo', 'bar')
|
||
|
batch.write(function (err) {
|
||
|
t.ifError(err)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#write() extensibility with null options', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
let batch
|
||
|
|
||
|
const Test = implement(AbstractChainedBatch, {
|
||
|
_write: function (options, callback) {
|
||
|
t.same(options, {})
|
||
|
t.is(this, batch, 'thisArg on _write() is correct')
|
||
|
this.nextTick(callback)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
batch = new Test(db)
|
||
|
|
||
|
// Without any operations, _write isn't called
|
||
|
batch.put('foo', 'bar')
|
||
|
batch.write(null, function (err) {
|
||
|
t.ifError(err)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#write() extensibility with options', function (t) {
|
||
|
t.plan(3)
|
||
|
|
||
|
let batch
|
||
|
|
||
|
const Test = implement(AbstractChainedBatch, {
|
||
|
_write: function (options, callback) {
|
||
|
t.same(options, { test: true })
|
||
|
t.is(this, batch, 'thisArg on _write() is correct')
|
||
|
this.nextTick(callback)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
batch = new Test(db)
|
||
|
// Without any operations, _write isn't called
|
||
|
batch.put('foo', 'bar')
|
||
|
batch.write({ test: true }, function (err) {
|
||
|
t.ifError(err)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#put() extensibility', function (t) {
|
||
|
t.plan(7)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedKey = 'key'
|
||
|
const expectedValue = 'value'
|
||
|
const Test = implement(AbstractChainedBatch, { _put: spy })
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
const test = new Test(db)
|
||
|
const returnValue = test.put(expectedKey, expectedValue)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _put call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _put() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 3, 'got 3 arguments')
|
||
|
t.equal(spy.getCall(0).args[0], expectedKey, 'got expected key argument')
|
||
|
t.equal(spy.getCall(0).args[1], expectedValue, 'got expected value argument')
|
||
|
t.same(spy.getCall(0).args[2], { keyEncoding: 'utf8', valueEncoding: 'utf8' }, 'got expected options argument')
|
||
|
t.equal(returnValue, test, 'get expected return value')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#del() extensibility', function (t) {
|
||
|
t.plan(6)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const expectedKey = 'key'
|
||
|
const Test = implement(AbstractChainedBatch, { _del: spy })
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
const test = new Test(db)
|
||
|
const returnValue = test.del(expectedKey)
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _del call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _del() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 2, 'got 2 arguments')
|
||
|
t.equal(spy.getCall(0).args[0], expectedKey, 'got expected key argument')
|
||
|
t.same(spy.getCall(0).args[1], { keyEncoding: 'utf8' }, 'got expected options argument')
|
||
|
t.equal(returnValue, test, 'get expected return value')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test AbstractChainedBatch#clear() extensibility', function (t) {
|
||
|
t.plan(4)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const Test = implement(AbstractChainedBatch, { _clear: spy })
|
||
|
const db = testCommon.factory()
|
||
|
|
||
|
db.once('open', function () {
|
||
|
const test = new Test(db)
|
||
|
const returnValue = test.clear()
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _clear call')
|
||
|
t.equal(spy.getCall(0).thisValue, test, '`this` on _clear() was correct')
|
||
|
t.equal(spy.getCall(0).args.length, 0, 'got zero arguments')
|
||
|
t.equal(returnValue, test, 'get expected return value')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('test clear() extensibility', function (t) {
|
||
|
t.plan((7 * 5) - 4)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const Test = implement(AbstractLevel, { _clear: spy })
|
||
|
const db = new Test({ encodings: { utf8: true } })
|
||
|
const callback = function () {}
|
||
|
|
||
|
db.once('open', function () {
|
||
|
call([callback], { keyEncoding: 'utf8', reverse: false, limit: -1 })
|
||
|
call([null, callback], { keyEncoding: 'utf8', reverse: false, limit: -1 })
|
||
|
call([undefined, callback], { keyEncoding: 'utf8', reverse: false, limit: -1 })
|
||
|
call([{ custom: 1 }, callback], { custom: 1, keyEncoding: 'utf8', reverse: false, limit: -1 })
|
||
|
call([{ reverse: true, limit: 0 }, callback], { keyEncoding: 'utf8', reverse: true, limit: 0 }, true)
|
||
|
call([{ reverse: 1 }, callback], { keyEncoding: 'utf8', reverse: true, limit: -1 })
|
||
|
call([{ reverse: null }, callback], { keyEncoding: 'utf8', reverse: false, limit: -1 })
|
||
|
|
||
|
function call (args, expectedOptions, shouldSkipCall) {
|
||
|
db.clear.apply(db, args)
|
||
|
|
||
|
t.is(spy.callCount, shouldSkipCall ? 0 : 1, 'got _clear() call')
|
||
|
|
||
|
if (!shouldSkipCall) {
|
||
|
t.is(spy.getCall(0).thisValue, db, '`this` on _clear() was correct')
|
||
|
t.is(spy.getCall(0).args.length, 2, 'got two arguments')
|
||
|
t.same(spy.getCall(0).args[0], expectedOptions, 'got expected options argument')
|
||
|
t.is(typeof spy.getCall(0).args[1], 'function', 'got callback argument')
|
||
|
}
|
||
|
|
||
|
spy.resetHistory()
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// TODO: replace with encoding test
|
||
|
test.skip('test serialization extensibility (batch array is not mutated)', function (t) {
|
||
|
t.plan(7)
|
||
|
|
||
|
const spy = sinon.spy()
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_batch: spy,
|
||
|
_serializeKey: function (key) {
|
||
|
t.equal(key, 'no')
|
||
|
return 'foo'
|
||
|
},
|
||
|
_serializeValue: function (value) {
|
||
|
t.equal(value, 'nope')
|
||
|
return 'bar'
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.once('open', function () {
|
||
|
const op = { type: 'put', key: 'no', value: 'nope' }
|
||
|
|
||
|
test.batch([op], function () {})
|
||
|
|
||
|
t.equal(spy.callCount, 1, 'got _batch() call')
|
||
|
t.equal(spy.getCall(0).args[0][0].key, 'foo', 'got expected key')
|
||
|
t.equal(spy.getCall(0).args[0][0].value, 'bar', 'got expected value')
|
||
|
|
||
|
t.equal(op.key, 'no', 'did not mutate input key')
|
||
|
t.equal(op.value, 'nope', 'did not mutate input value')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('clear() does not delete empty or nullish range options', function (t) {
|
||
|
const rangeValues = [Uint8Array.from([]), '', null, undefined]
|
||
|
|
||
|
t.plan(rangeOptions.length * rangeValues.length)
|
||
|
|
||
|
rangeValues.forEach(function (value) {
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_clear: function (options, callback) {
|
||
|
rangeOptions.forEach(function (key) {
|
||
|
t.ok(key in options, key + ' option should not be deleted')
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const db = new Test({ encodings: { utf8: true } })
|
||
|
const options = {}
|
||
|
|
||
|
rangeOptions.forEach(function (key) {
|
||
|
options[key] = value
|
||
|
})
|
||
|
|
||
|
db.once('open', function () {
|
||
|
db.clear(options, function () {})
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// TODO: some of these are redundant
|
||
|
test('.status', function (t) {
|
||
|
t.plan(5)
|
||
|
|
||
|
t.test('empty prototype', function (t) {
|
||
|
const Test = implement(AbstractLevel)
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
t.equal(test.status, 'opening')
|
||
|
|
||
|
test.open(function (err) {
|
||
|
t.error(err)
|
||
|
t.equal(test.status, 'open')
|
||
|
|
||
|
test.close(function (err) {
|
||
|
t.error(err)
|
||
|
t.equal(test.status, 'closed')
|
||
|
t.end()
|
||
|
})
|
||
|
})
|
||
|
|
||
|
t.equal(test.status, 'opening')
|
||
|
})
|
||
|
|
||
|
t.test('open error', function (t) {
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_open: function (options, cb) {
|
||
|
cb(new Error('_open error'))
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
|
||
|
test.open(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN')
|
||
|
t.is(err && err.cause && err.cause.message, '_open error')
|
||
|
t.equal(test.status, 'closed')
|
||
|
t.end()
|
||
|
})
|
||
|
})
|
||
|
|
||
|
t.test('close error', function (t) {
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_close: function (cb) {
|
||
|
cb(new Error('_close error'))
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
test.open(function () {
|
||
|
test.close(function (err) {
|
||
|
t.is(err && err.code, 'LEVEL_DATABASE_NOT_CLOSED')
|
||
|
t.is(err && err.cause && err.cause.message, '_close error')
|
||
|
t.equal(test.status, 'open')
|
||
|
t.end()
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
t.test('open', function (t) {
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_open: function (options, cb) {
|
||
|
this.nextTick(cb)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
test.open(function (err) {
|
||
|
t.error(err)
|
||
|
t.equal(test.status, 'open')
|
||
|
t.end()
|
||
|
})
|
||
|
t.equal(test.status, 'opening')
|
||
|
})
|
||
|
|
||
|
t.test('close', function (t) {
|
||
|
const Test = implement(AbstractLevel, {
|
||
|
_close: function (cb) {
|
||
|
this.nextTick(cb)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const test = new Test({ encodings: { utf8: true } })
|
||
|
test.open(function (err) {
|
||
|
t.error(err)
|
||
|
test.close(function (err) {
|
||
|
t.error(err)
|
||
|
t.equal(test.status, 'closed')
|
||
|
t.end()
|
||
|
})
|
||
|
t.equal(test.status, 'closing')
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
test('rangeOptions', function (t) {
|
||
|
const keys = rangeOptions.slice()
|
||
|
const db = new AbstractLevel({
|
||
|
encodings: {
|
||
|
utf8: true, buffer: true, view: true
|
||
|
}
|
||
|
})
|
||
|
|
||
|
function setupOptions (create) {
|
||
|
const options = {}
|
||
|
for (const key of keys) {
|
||
|
options[key] = create()
|
||
|
}
|
||
|
return options
|
||
|
}
|
||
|
|
||
|
function verifyOptions (t, options) {
|
||
|
for (const key of keys) {
|
||
|
t.ok(key in options, key + ' option should not be deleted')
|
||
|
}
|
||
|
t.end()
|
||
|
}
|
||
|
|
||
|
t.plan(11)
|
||
|
t.test('setup', async (t) => db.open())
|
||
|
|
||
|
t.test('default options', function (t) {
|
||
|
t.same(getRangeOptions(undefined, db.keyEncoding('utf8')), {
|
||
|
reverse: false,
|
||
|
limit: -1
|
||
|
}, 'correct defaults')
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
t.test('set options', function (t) {
|
||
|
t.same(getRangeOptions({ reverse: false, limit: 20 }, db.keyEncoding('utf8')), {
|
||
|
reverse: false,
|
||
|
limit: 20
|
||
|
}, 'options set correctly')
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
t.test('ignores invalid limit', function (t) {
|
||
|
// Infinity is valid but is normalized to -1 for use in private API
|
||
|
for (const limit of [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, NaN, -2, 5.5]) {
|
||
|
t.same(getRangeOptions({ limit }, db.keyEncoding('utf8')).limit, -1)
|
||
|
}
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
t.test('ignores not-own property', function (t) {
|
||
|
class Options {}
|
||
|
Options.prototype.limit = 20
|
||
|
const options = new Options()
|
||
|
|
||
|
t.is(options.limit, 20)
|
||
|
t.same(getRangeOptions(options, db.keyEncoding('utf8')), {
|
||
|
reverse: false,
|
||
|
limit: -1
|
||
|
})
|
||
|
t.end()
|
||
|
})
|
||
|
|
||
|
t.test('does not delete empty buffers', function (t) {
|
||
|
const options = setupOptions(() => Buffer.alloc(0))
|
||
|
keys.forEach(function (key) {
|
||
|
t.is(isBuffer(options[key]), true, 'should be buffer')
|
||
|
t.is(options[key].byteLength, 0, 'should be empty')
|
||
|
})
|
||
|
verifyOptions(t, getRangeOptions(options, db.keyEncoding('buffer')))
|
||
|
})
|
||
|
|
||
|
t.test('does not delete empty views', function (t) {
|
||
|
const options = setupOptions(() => Uint8Array.from([]))
|
||
|
keys.forEach(function (key) {
|
||
|
t.is(options[key] instanceof Uint8Array, true, 'should be Uint8Array')
|
||
|
t.is(options[key].byteLength, 0, 'should be empty')
|
||
|
})
|
||
|
verifyOptions(t, getRangeOptions(options, db.keyEncoding('view')))
|
||
|
})
|
||
|
|
||
|
t.test('does not delete empty strings', function (t) {
|
||
|
const options = setupOptions(() => '')
|
||
|
keys.forEach(function (key) {
|
||
|
t.is(typeof options[key], 'string', 'should be string')
|
||
|
t.is(options[key].length, 0, 'should be empty')
|
||
|
})
|
||
|
verifyOptions(t, getRangeOptions(options, db.keyEncoding('utf8')))
|
||
|
})
|
||
|
|
||
|
t.test('does not delete null', function (t) {
|
||
|
const options = setupOptions(() => null)
|
||
|
keys.forEach(function (key) {
|
||
|
t.is(options[key], null)
|
||
|
})
|
||
|
verifyOptions(t, getRangeOptions(options, db.keyEncoding('utf8')))
|
||
|
})
|
||
|
|
||
|
t.test('does not delete undefined', function (t) {
|
||
|
const options = setupOptions(() => undefined)
|
||
|
keys.forEach(function (key) {
|
||
|
t.is(options[key], undefined)
|
||
|
})
|
||
|
verifyOptions(t, getRangeOptions(options, db.keyEncoding('utf8')))
|
||
|
})
|
||
|
|
||
|
t.test('rejects legacy range options', function (t) {
|
||
|
t.plan(2)
|
||
|
|
||
|
for (const key of ['start', 'end']) {
|
||
|
const options = {}
|
||
|
options[key] = 'x'
|
||
|
|
||
|
try {
|
||
|
getRangeOptions(options, db.keyEncoding('utf8'))
|
||
|
} catch (err) {
|
||
|
t.is(err.code, 'LEVEL_LEGACY')
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
require('./self/defer-test')
|
||
|
require('./self/attach-resource-test')
|
||
|
require('./self/abstract-iterator-test')
|
||
|
require('./self/iterator-test')
|
||
|
require('./self/deferred-iterator-test')
|
||
|
require('./self/deferred-operations-test')
|
||
|
require('./self/deferred-chained-batch-test')
|
||
|
require('./self/async-iterator-test')
|
||
|
require('./self/encoding-test')
|
||
|
require('./self/sublevel-test')
|
||
|
|
||
|
// Test the abstract test suite using a minimal implementation
|
||
|
require('./index')({
|
||
|
test,
|
||
|
factory (options) {
|
||
|
return new MinimalLevel(options)
|
||
|
}
|
||
|
})
|