281 lines
7.6 KiB
JavaScript

'use strict'
const ModuleError = require('module-error')
const { AbstractLevel, AbstractChainedBatch } = require('..')
const { AbstractIterator, AbstractKeyIterator, AbstractValueIterator } = require('..')
const spies = []
exports.verifyNotFoundError = function (err) {
return err.code === 'LEVEL_NOT_FOUND' && err.notFound === true && err.status === 404
}
exports.illegalKeys = [
{ name: 'null key', key: null },
{ name: 'undefined key', key: undefined }
]
exports.illegalValues = [
{ name: 'null key', value: null },
{ name: 'undefined value', value: undefined }
]
/**
* Wrap a callback to check that it's called asynchronously. Must be
* combined with a `ctx()`, `with()` or `end()` call.
*
* @param {function} cb Callback to check.
* @param {string} name Optional callback name to use in assertion messages.
* @returns {function} Wrapped callback.
*/
exports.assertAsync = function (cb, name) {
const spy = {
called: false,
name: name || cb.name || 'anonymous'
}
spies.push(spy)
return function (...args) {
spy.called = true
return cb.apply(this, args)
}
}
/**
* Verify that callbacks wrapped with `assertAsync()` were not yet called.
* @param {import('tape').Test} t Tape test object.
*/
exports.assertAsync.end = function (t) {
for (const { called, name } of spies.splice(0, spies.length)) {
t.is(called, false, `callback (${name}) is asynchronous`)
}
}
/**
* Wrap a test function to verify `assertAsync()` spies at the end.
* @param {import('tape').TestCase} test Test function to be passed to `tape()`.
* @returns {import('tape').TestCase} Wrapped test function.
*/
exports.assertAsync.ctx = function (test) {
return function (...args) {
const ret = test.call(this, ...args)
exports.assertAsync.end(args[0])
return ret
}
}
/**
* Wrap an arbitrary callback to verify `assertAsync()` spies at the end.
* @param {import('tape').Test} t Tape test object.
* @param {function} cb Callback to wrap.
* @returns {function} Wrapped callback.
*/
exports.assertAsync.with = function (t, cb) {
return function (...args) {
const ret = cb.call(this, ...args)
exports.assertAsync.end(t)
return ret
}
}
exports.mockLevel = function (methods, ...args) {
class TestLevel extends AbstractLevel {}
for (const k in methods) TestLevel.prototype[k] = methods[k]
if (!args.length) args = [{ encodings: { utf8: true } }]
return new TestLevel(...args)
}
exports.mockIterator = function (db, options, methods, ...args) {
class TestIterator extends AbstractIterator {}
for (const k in methods) TestIterator.prototype[k] = methods[k]
return new TestIterator(db, options, ...args)
}
exports.mockChainedBatch = function (db, methods, ...args) {
class TestBatch extends AbstractChainedBatch {}
for (const k in methods) TestBatch.prototype[k] = methods[k]
return new TestBatch(db, ...args)
}
// Mock encoding where null and undefined are significant types
exports.nullishEncoding = {
name: 'nullish',
format: 'utf8',
encode (v) {
return v === null ? '\x00' : v === undefined ? '\xff' : String(v)
},
decode (v) {
return v === '\x00' ? null : v === '\xff' ? undefined : v
}
}
const kEntries = Symbol('entries')
const kPosition = Symbol('position')
const kOptions = Symbol('options')
/**
* A minimal and non-optimized implementation for use in tests. Only supports utf8.
* Don't use this as a reference implementation.
*/
class MinimalLevel extends AbstractLevel {
constructor (options) {
super({ encodings: { utf8: true }, seek: true }, options)
this[kEntries] = new Map()
}
_put (key, value, options, callback) {
this[kEntries].set(key, value)
this.nextTick(callback)
}
_get (key, options, callback) {
const value = this[kEntries].get(key)
if (value === undefined) {
return this.nextTick(callback, new ModuleError(`Key ${key} was not found`, {
code: 'LEVEL_NOT_FOUND'
}))
}
this.nextTick(callback, null, value)
}
_getMany (keys, options, callback) {
const values = keys.map(k => this[kEntries].get(k))
this.nextTick(callback, null, values)
}
_del (key, options, callback) {
this[kEntries].delete(key)
this.nextTick(callback)
}
_clear (options, callback) {
for (const [k] of sliceEntries(this[kEntries], options, true)) {
this[kEntries].delete(k)
}
this.nextTick(callback)
}
_batch (operations, options, callback) {
const entries = new Map(this[kEntries])
for (const op of operations) {
if (op.type === 'put') entries.set(op.key, op.value)
else entries.delete(op.key)
}
this[kEntries] = entries
this.nextTick(callback)
}
_iterator (options) {
return new MinimalIterator(this, options)
}
_keys (options) {
return new MinimalKeyIterator(this, options)
}
_values (options) {
return new MinimalValueIterator(this, options)
}
}
class MinimalIterator extends AbstractIterator {
constructor (db, options) {
super(db, options)
this[kEntries] = sliceEntries(db[kEntries], options, false)
this[kOptions] = options
this[kPosition] = 0
}
}
class MinimalKeyIterator extends AbstractKeyIterator {
constructor (db, options) {
super(db, options)
this[kEntries] = sliceEntries(db[kEntries], options, false)
this[kOptions] = options
this[kPosition] = 0
}
}
class MinimalValueIterator extends AbstractValueIterator {
constructor (db, options) {
super(db, options)
this[kEntries] = sliceEntries(db[kEntries], options, false)
this[kOptions] = options
this[kPosition] = 0
}
}
for (const Ctor of [MinimalIterator, MinimalKeyIterator, MinimalValueIterator]) {
const mapEntry = Ctor === MinimalIterator ? e => e : Ctor === MinimalKeyIterator ? e => e[0] : e => e[1]
Ctor.prototype._next = function (callback) {
const entry = this[kEntries][this[kPosition]++]
if (entry === undefined) return this.nextTick(callback)
if (Ctor === MinimalIterator) this.nextTick(callback, null, entry[0], entry[1])
else this.nextTick(callback, null, mapEntry(entry))
}
Ctor.prototype._nextv = function (size, options, callback) {
const entries = this[kEntries].slice(this[kPosition], this[kPosition] + size)
this[kPosition] += entries.length
this.nextTick(callback, null, entries.map(mapEntry))
}
Ctor.prototype._all = function (options, callback) {
const end = this.limit - this.count + this[kPosition]
const entries = this[kEntries].slice(this[kPosition], end)
this[kPosition] = this[kEntries].length
this.nextTick(callback, null, entries.map(mapEntry))
}
Ctor.prototype._seek = function (target, options) {
this[kPosition] = this[kEntries].length
if (!outOfRange(target, this[kOptions])) {
// Don't care about performance here
for (let i = 0; i < this[kPosition]; i++) {
const key = this[kEntries][i][0]
if (this[kOptions].reverse ? key <= target : key >= target) {
this[kPosition] = i
}
}
}
}
}
const outOfRange = function (target, options) {
if ('gte' in options) {
if (target < options.gte) return true
} else if ('gt' in options) {
if (target <= options.gt) return true
}
if ('lte' in options) {
if (target > options.lte) return true
} else if ('lt' in options) {
if (target >= options.lt) return true
}
return false
}
const sliceEntries = function (entries, options, applyLimit) {
entries = Array.from(entries)
.filter((e) => !outOfRange(e[0], options))
.sort((a, b) => a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0)
if (options.reverse) entries.reverse()
if (applyLimit && options.limit !== -1) entries = entries.slice(0, options.limit)
return entries
}
exports.MinimalLevel = MinimalLevel