From e4445a7f07c651706728b602b8dc1f967a06955f Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Tue, 28 Sep 2021 22:34:46 +0200 Subject: [PATCH] Add `db.getMany(keys)` (#381) Ref Level/community#101 --- README.md | 21 ++- abstract-leveldown.js | 68 +++++++++ lib/common.js | 9 ++ package.json | 4 +- test/common.js | 2 +- test/del-test.js | 16 -- test/get-many-test.js | 309 +++++++++++++++++++++++++++++++++++++++ test/get-test.js | 65 +++----- test/index.js | 4 + test/iterator-test.js | 4 +- test/put-get-del-test.js | 34 ++++- test/put-test.js | 30 ---- test/self.js | 74 ++++++++++ test/util.js | 60 ++++++++ 14 files changed, 598 insertions(+), 102 deletions(-) create mode 100644 lib/common.js create mode 100644 test/get-many-test.js diff --git a/README.md b/README.md index 147bc60..4f9b90c 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,17 @@ Get a value from the store by `key`. The optional `options` object may contain: - `asBuffer` _(boolean, default: `true`)_: Whether to return the `value` as a Buffer. If `false`, the returned type depends on the implementation. -The `callback` function will be called with an `Error` if the operation failed for any reason. If successful the first argument will be `null` and the second argument will be the value. +The `callback` function will be called with an `Error` if the operation failed for any reason, including if the key was not found. If successful the first argument will be `null` and the second argument will be the value. + +### `db.getMany(keys[, options][, callback])` + +Get multiple values from the store by an array of `keys`. The optional `options` object may contain: + +- `asBuffer` _(boolean, default: `true`)_: Whether to return the `value` as a Buffer. If `false`, the returned type depends on the implementation. + +The `callback` function will be called with an `Error` if the operation failed for any reason. If successful the first argument will be `null` and the second argument will be an array of values with the same order as `keys`. If a key was not found, the relevant value will be `undefined`. + +If no callback is provided, a promise is returned. ### `db.put(key, value[, options], callback)` @@ -435,6 +445,14 @@ Get a value by `key`. The `options` object will always have the following proper The default `_get()` invokes `callback` on a next tick with a `NotFound` error. It must be overridden. +### `db._getMany(keys, options, callback)` + +**This new method is optional for the time being. To enable its tests, set the [`getMany` option of the test suite](#excluding-tests) to `true`.** + +Get multiple values by an array of `keys`. The `options` object will always have the following properties: `asBuffer`. If an error occurs, call the `callback` function with an `Error`. Otherwise call `callback` with `null` as the first argument and an array of values as the second. If a key does not exist, set the relevant value to `undefined`. + +The default `_getMany()` invokes `callback` on a next tick with an array of values that is equal in length to `keys` and is filled with `undefined`. It must be overridden to support `getMany()` but this is currently an opt-in feature. If the implementation does support `getMany()` then `db.supports.getMany` must be set to true via the [constructor](#db--abstractleveldownmanifest). + ### `db._put(key, value, options, callback)` Store a new entry or overwrite an existing entry. There are no default options but `options` will always be an object. If putting failed, call the `callback` function with an `Error`. Otherwise call `callback` without any arguments. @@ -581,6 +599,7 @@ This also serves as a signal to users of your implementation. The following opti - `bufferKeys`: set to `false` if binary keys are not supported by the underlying storage - `seek`: set to `false` if your `iterator` does not implement `_seek` - `clear`: defaults to `false` until a next major release. Set to `true` if your implementation either implements `_clear()` itself or is suitable to use the default implementation of `_clear()` (which requires binary key support). +- `getMany`: defaults to `false` until a next major release. Set to `true` if your implementation implements `_getMany()`. - `snapshots`: set to `false` if any of the following is true: - Reads don't operate on a [snapshot](#iterator) - Snapshots are created asynchronously diff --git a/abstract-leveldown.js b/abstract-leveldown.js index eaa5129..8d3b329 100644 --- a/abstract-leveldown.js +++ b/abstract-leveldown.js @@ -2,8 +2,12 @@ const supports = require('level-supports') const isBuffer = require('is-buffer') +const catering = require('catering') const AbstractIterator = require('./abstract-iterator') const AbstractChainedBatch = require('./abstract-chained-batch') +const getCallback = require('./lib/common').getCallback +const getOptions = require('./lib/common').getOptions + const hasOwnProperty = Object.prototype.hasOwnProperty const rangeOptions = ['lt', 'lte', 'gt', 'gte'] @@ -90,6 +94,51 @@ AbstractLevelDOWN.prototype._get = function (key, options, callback) { this._nextTick(function () { callback(new Error('NotFound')) }) } +AbstractLevelDOWN.prototype.getMany = function (keys, options, callback) { + callback = getCallback(options, callback) + callback = catering.fromCallback(callback) + options = getOptions(options) + + if (maybeError(this, callback)) { + return callback.promise + } + + if (!Array.isArray(keys)) { + this._nextTick(callback, new Error('getMany() requires an array argument')) + return callback.promise + } + + if (keys.length === 0) { + this._nextTick(callback, null, []) + return callback.promise + } + + if (typeof options.asBuffer !== 'boolean') { + options = { ...options, asBuffer: true } + } + + const serialized = new Array(keys.length) + + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const err = this._checkKey(key) + + if (err) { + this._nextTick(callback, err) + return callback.promise + } + + serialized[i] = this._serializeKey(key) + } + + this._getMany(serialized, options, callback) + return callback.promise +} + +AbstractLevelDOWN.prototype._getMany = function (keys, options, callback) { + this._nextTick(callback, null, new Array(keys.length).fill(undefined)) +} + AbstractLevelDOWN.prototype.put = function (key, value, options, callback) { if (typeof options === 'function') callback = options @@ -315,9 +364,28 @@ AbstractLevelDOWN.prototype._checkValue = function (value) { } } +// TODO: docs and tests +AbstractLevelDOWN.prototype.isOperational = function () { + return this.status === 'open' || this._isOperational() +} + +// Implementation may accept operations in other states too +AbstractLevelDOWN.prototype._isOperational = function () { + return false +} + // Expose browser-compatible nextTick for dependents // TODO: rename _nextTick to _queueMicrotask // TODO: after we drop node 10, also use queueMicrotask in node AbstractLevelDOWN.prototype._nextTick = require('./next-tick') module.exports = AbstractLevelDOWN + +function maybeError (db, callback) { + if (!db.isOperational()) { + db._nextTick(callback, new Error('Database is not open')) + return true + } + + return false +} diff --git a/lib/common.js b/lib/common.js new file mode 100644 index 0000000..c354fa8 --- /dev/null +++ b/lib/common.js @@ -0,0 +1,9 @@ +'use strict' + +exports.getCallback = function (options, callback) { + return typeof options === 'function' ? options : callback +} + +exports.getOptions = function (options) { + return typeof options === 'object' && options !== null ? options : {} +} diff --git a/package.json b/package.json index edf63a9..122b226 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "abstract-iterator.js", "abstract-leveldown.js", "index.js", + "lib", "next-tick-browser.js", "next-tick.js", "test", @@ -30,9 +31,10 @@ ], "dependencies": { "buffer": "^6.0.3", + "catering": "^2.0.0", "is-buffer": "^2.0.5", "level-concat-iterator": "^3.0.0", - "level-supports": "^2.0.0", + "level-supports": "^2.0.1", "queue-microtask": "^1.2.3" }, "devDependencies": { diff --git a/test/common.js b/test/common.js index dfb3a6e..4f0a72a 100644 --- a/test/common.js +++ b/test/common.js @@ -31,6 +31,7 @@ function testCommon (options) { snapshots: options.snapshots !== false, seek: options.seek !== false, clear: !!options.clear, + getMany: !!options.getMany, // Support running test suite on a levelup db. All options below this line // are undocumented and should not be used by abstract-leveldown db's (yet). @@ -42,7 +43,6 @@ function testCommon (options) { // and that operations return strings rather than buffers by default. encodings: !!options.encodings, - // Not yet used, only here for symmetry with levelup's test suite. deferredOpen: !!options.deferredOpen, streams: !!options.streams } diff --git a/test/del-test.js b/test/del-test.js index a2fffca..96a6a5f 100644 --- a/test/del-test.js +++ b/test/del-test.js @@ -39,22 +39,6 @@ exports.args = function (test, testCommon) { ) t.end() }) - - testCommon.serialize && test('test custom _serialize*', function (t) { - t.plan(3) - const db = testCommon.factory() - db._serializeKey = function (data) { return data } - db._del = function (key, options, callback) { - t.deepEqual(key, { foo: 'bar' }) - this._nextTick(callback) - } - db.open(function () { - db.del({ foo: 'bar' }, function (err) { - t.error(err) - db.close(t.error.bind(t)) - }) - }) - }) } exports.del = function (test, testCommon) { diff --git a/test/get-many-test.js b/test/get-many-test.js new file mode 100644 index 0000000..ab1f451 --- /dev/null +++ b/test/get-many-test.js @@ -0,0 +1,309 @@ +'use strict' + +const isBuffer = require('is-buffer') +const isTypedArray = require('./util').isTypedArray +const assertAsync = require('./util').assertAsync + +let db + +/** + * @param {import('tape')} test + */ +exports.setUp = function (test, testCommon) { + test('setUp db', function (t) { + db = testCommon.factory() + db.open(t.end.bind(t)) + }) +} + +/** + * @param {import('tape')} test + */ +exports.args = function (test, testCommon) { + test('test getMany() requires an array argument (callback)', assertAsync.ctx(function (t) { + // Add 1 assertion for every assertAsync() + t.plan(4) + + db.getMany('foo', assertAsync(function (err) { + t.is(err && err.message, 'getMany() requires an array argument') + })) + db.getMany('foo', {}, assertAsync(function (err) { + t.is(err && err.message, 'getMany() requires an array argument') + })) + })) + + test('test getMany() requires an array argument (promise)', function (t) { + t.plan(3) + + db.getMany().catch(function (err) { + t.is(err && err.message, 'getMany() requires an array argument') + }) + db.getMany('foo').catch(function (err) { + t.is(err && err.message, 'getMany() requires an array argument') + }) + db.getMany('foo', {}).catch(function (err) { + t.is(err && err.message, 'getMany() requires an array argument') + }) + }) +} + +/** + * @param {import('tape')} test + */ +exports.getMany = function (test, testCommon) { + test('test getMany() support is reflected in manifest', function (t) { + t.is(db.supports && db.supports.getMany, true) + t.end() + }) + + test('test simple getMany()', function (t) { + db.put('foo', 'bar', function (err) { + t.error(err) + + function verify (err, values) { + t.error(err) + t.ok(Array.isArray(values), 'got an array') + t.is(values.length, 1, 'array has 1 element') + + const value = values[0] + let result + + if (!testCommon.encodings) { + t.isNot(typeof value, 'string', 'should not be string by default') + + if (isTypedArray(value)) { + result = String.fromCharCode.apply(null, new Uint16Array(value)) + } else { + t.ok(isBuffer(value)) + try { + result = value.toString() + } catch (e) { + t.error(e, 'should not throw when converting value to a string') + } + } + } else { + result = value + } + + t.is(result, 'bar') + } + + db.getMany(['foo'], function (err, values) { + verify(err, values) + + db.getMany(['foo'], {}, function (err, values) { + verify(err, values) + + db.getMany(['foo'], { asBuffer: false }, function (err, values) { + t.error(err) + t.is(values && typeof values[0], 'string', 'should be string if not buffer') + t.same(values, ['bar']) + t.end() + }) + }) + }) + }) + }) + + test('test getMany() with multiple keys', function (t) { + t.plan(5) + + db.put('beep', 'boop', function (err) { + t.ifError(err) + + db.getMany(['foo', 'beep'], { asBuffer: false }, function (err, values) { + t.ifError(err) + t.same(values, ['bar', 'boop']) + }) + + db.getMany(['beep', 'foo'], { asBuffer: false }, function (err, values) { + t.ifError(err) + t.same(values, ['boop', 'bar'], 'maintains order of input keys') + }) + }) + }) + + test('test empty getMany()', assertAsync.ctx(function (t) { + t.plan(2 * 3) + + for (const asBuffer in [true, false]) { + db.getMany([], { asBuffer }, assertAsync(function (err, values) { + t.ifError(err) + t.same(values, []) + })) + } + })) + + test('test not-found getMany()', assertAsync.ctx(function (t) { + t.plan(2 * 3) + + for (const asBuffer in [true, false]) { + db.getMany(['nope', 'another'], { asBuffer }, assertAsync(function (err, values) { + t.ifError(err) + t.same(values, [undefined, undefined]) + })) + } + })) + + test('test getMany() with promise', async function (t) { + t.same(await db.getMany(['foo'], { asBuffer: false }), ['bar']) + t.same(await db.getMany(['beep'], { asBuffer: false }), ['boop']) + t.same(await db.getMany(['foo', 'beep'], { asBuffer: false }), ['bar', 'boop']) + t.same(await db.getMany(['beep', 'foo'], { asBuffer: false }), ['boop', 'bar']) + t.same(await db.getMany(['beep', 'foo', 'nope'], { asBuffer: false }), ['boop', 'bar', undefined]) + t.same(await db.getMany([], { asBuffer: false }), []) + }) + + test('test simultaneous getMany()', function (t) { + db.put('hello', 'world', function (err) { + t.error(err) + + let completed = 0 + const done = function () { + if (++completed === 20) t.end() + } + + for (let i = 0; i < 10; ++i) { + db.getMany(['hello'], function (err, values) { + t.error(err) + t.is(values.length, 1) + t.is(values[0] && values[0].toString(), 'world') + done() + }) + } + + for (let i = 0; i < 10; ++i) { + db.getMany(['not found'], function (err, values) { + t.error(err) + t.same(values, [undefined]) + done() + }) + } + }) + }) + + test('test getMany() on new db', assertAsync.ctx(function (t) { + t.plan(2 * 2 * 5) + + // Also test empty array because it has a fast-path + for (const keys of [['foo'], []]) { + // Opening should make no difference, because we call it after getMany() + for (const open of [true, false]) { + const db = testCommon.factory() + + if (testCommon.status) { + t.is(db.status, testCommon.deferredOpen ? 'opening' : 'new') + } else { + t.pass('no status') + } + + // Must be true if db supports deferredOpen + const operational = testCommon.deferredOpen || db.isOperational() + + db.getMany(keys, assertAsync(function (err, values) { + if (operational) { + t.ifError(err, 'no error') + t.same(values, keys.map(_ => undefined)) + } else { + t.is(err && err.message, 'Database is not open') + t.is(values, undefined) + } + })) + + if (open) { + db.open(t.error.bind(t)) + } else { + t.pass() + } + } + } + })) + + test('test getMany() on opening db', assertAsync.ctx(function (t) { + t.plan(2 * 5) + + // Also test empty array because it has a fast-path + for (const keys of [['foo'], []]) { + const db = testCommon.factory() + + // Is a noop if db supports deferredOpen + db.open(assertAsync(t.error.bind(t), 'open')) + + // Must be true if db supports deferredOpen + const operational = testCommon.deferredOpen || db.isOperational() + + db.getMany(keys, assertAsync(function (err, values) { + if (operational) { + t.ifError(err, 'no error') + t.same(values, keys.map(_ => undefined)) + } else { + t.is(err && err.message, 'Database is not open') + t.is(values, undefined) + } + })) + } + })) + + test('test getMany() on closed db', function (t) { + t.plan(2 * 6) + + // Also test empty array because it has a fast-path + for (const keys of [['foo'], []]) { + const db = testCommon.factory() + + db.open(function (err) { + t.ifError(err) + t.is(db.isOperational(), true) + + db.close(assertAsync.with(t, function (err) { + t.ifError(err) + t.is(db.isOperational(), false) + + db.getMany(keys, assertAsync(function (err) { + t.is(err && err.message, 'Database is not open') + })) + })) + }) + } + }) + + test('test getMany() on closing db', function (t) { + t.plan(2 * 4) + + // Also test empty array because it has a fast-path + for (const keys of [['foo'], []]) { + const db = testCommon.factory() + + db.open(assertAsync.with(t, function (err) { + t.ifError(err) + + db.close(function (err) { + t.ifError(err) + }) + + db.getMany(keys, assertAsync(function (err) { + t.is(err && err.message, 'Database is not open') + })) + })) + } + }) +} + +/** + * @param {import('tape')} test + */ +exports.tearDown = function (test, testCommon) { + test('tearDown', function (t) { + db.close(t.end.bind(t)) + }) +} + +/** + * @param {import('tape')} test + */ +exports.all = function (test, testCommon) { + exports.setUp(test, testCommon) + exports.args(test, testCommon) + exports.getMany(test, testCommon) + exports.tearDown(test, testCommon) +} diff --git a/test/get-test.js b/test/get-test.js index ac24aea..bd81f88 100644 --- a/test/get-test.js +++ b/test/get-test.js @@ -1,5 +1,6 @@ 'use strict' +const isBuffer = require('is-buffer') const verifyNotFoundError = require('./util').verifyNotFoundError const isTypedArray = require('./util').isTypedArray @@ -40,22 +41,6 @@ exports.args = function (test, testCommon) { ) t.end() }) - - testCommon.serialize && test('test custom _serialize*', function (t) { - t.plan(3) - const db = testCommon.factory() - db._serializeKey = function (data) { return data } - db._get = function (key, options, callback) { - t.deepEqual(key, { foo: 'bar' }) - this._nextTick(callback) - } - db.open(function () { - db.get({ foo: 'bar' }, function (err) { - t.error(err) - db.close(t.error.bind(t)) - }) - }) - }) } exports.get = function (test, testCommon) { @@ -68,12 +53,12 @@ exports.get = function (test, testCommon) { let result if (!testCommon.encodings) { - t.ok(typeof value !== 'string', 'should not be string by default') + t.isNot(typeof value, 'string', 'should not be string by default') if (isTypedArray(value)) { result = String.fromCharCode.apply(null, new Uint16Array(value)) } else { - t.ok(typeof Buffer !== 'undefined' && value instanceof Buffer) + t.ok(isBuffer(value)) try { result = value.toString() } catch (e) { @@ -84,7 +69,7 @@ exports.get = function (test, testCommon) { result = value } - t.equal(result, 'bar') + t.is(result, 'bar') db.get('foo', {}, function (err, value) { // same but with {} t.error(err) @@ -97,7 +82,7 @@ exports.get = function (test, testCommon) { if (isTypedArray(value)) { result = String.fromCharCode.apply(null, new Uint16Array(value)) } else { - t.ok(typeof Buffer !== 'undefined' && value instanceof Buffer) + t.ok(isBuffer(value)) try { result = value.toString() } catch (e) { @@ -108,12 +93,12 @@ exports.get = function (test, testCommon) { result = value } - t.equal(result, 'bar') + t.is(result, 'bar') db.get('foo', { asBuffer: false }, function (err, value) { t.error(err) t.ok(typeof value === 'string', 'should be string if not buffer') - t.equal(value, 'bar') + t.is(value, 'bar') t.end() }) }) @@ -121,25 +106,23 @@ exports.get = function (test, testCommon) { }) }) - test('test simultaniously get()', function (t) { + test('test simultaneous get()', function (t) { db.put('hello', 'world', function (err) { t.error(err) - let r = 0 + let completed = 0 const done = function () { - if (++r === 20) { t.end() } + if (++completed === 20) t.end() } - let i = 0 - let j = 0 - for (; i < 10; ++i) { + for (let i = 0; i < 10; ++i) { db.get('hello', function (err, value) { t.error(err) - t.equal(value.toString(), 'world') + t.is(value.toString(), 'world') done() }) } - for (; j < 10; ++j) { + for (let i = 0; i < 10; ++i) { db.get('not found', function (err, value) { t.ok(err, 'should error') t.ok(verifyNotFoundError(err), 'should have correct error message') @@ -151,22 +134,18 @@ exports.get = function (test, testCommon) { }) test('test get() not found error is asynchronous', function (t) { - t.plan(5) + t.plan(4) - db.put('hello', 'world', function (err) { - t.error(err) - - let async = false + let async = false - db.get('not found', function (err, value) { - t.ok(err, 'should error') - t.ok(verifyNotFoundError(err), 'should have correct error message') - t.ok(typeof value === 'undefined', 'value is undefined') - t.ok(async, 'callback is asynchronous') - }) - - async = true + db.get('not found', function (err, value) { + t.ok(err, 'should error') + t.ok(verifyNotFoundError(err), 'should have correct error message') + t.ok(typeof value === 'undefined', 'value is undefined') + t.ok(async, 'callback is asynchronous') }) + + async = true }) } diff --git a/test/index.js b/test/index.js index fbd65b2..d761d38 100644 --- a/test/index.js +++ b/test/index.js @@ -26,6 +26,10 @@ function suite (options) { require('./del-test').all(test, testCommon) require('./put-get-del-test').all(test, testCommon) + if (testCommon.getMany) { + require('./get-many-test').all(test, testCommon) + } + require('./batch-test').all(test, testCommon) require('./chained-batch-test').all(test, testCommon) diff --git a/test/iterator-test.js b/test/iterator-test.js index bf07130..c7b5177 100644 --- a/test/iterator-test.js +++ b/test/iterator-test.js @@ -13,8 +13,8 @@ exports.setUp = function (test, testCommon) { exports.args = function (test, testCommon) { test('test iterator has db reference', function (t) { const iterator = db.iterator() - // For levelup compat: may return iterator of an underlying db, that's okay. - t.ok(iterator.db === db || iterator.db) + // For levelup & deferred-leveldown compat: may return iterator of an underlying db, that's okay. + t.ok(iterator.db === db || iterator.db === (db.db || db._db || db)) iterator.end(t.end.bind(t)) }) diff --git a/test/put-get-del-test.js b/test/put-get-del-test.js index cb7783f..0bd21f9 100644 --- a/test/put-get-del-test.js +++ b/test/put-get-del-test.js @@ -1,11 +1,12 @@ 'use strict' const verifyNotFoundError = require('./util').verifyNotFoundError +const assertAsync = require('./util').assertAsync const testBuffer = Buffer.from('testbuffer') let db -function makeGetDelErrorTests (test, type, key, expectedError) { +function makeGetDelErrorTests (test, testCommon, type, key, expectedError) { test('test get() with ' + type + ' causes error', function (t) { let async = false @@ -33,6 +34,23 @@ function makeGetDelErrorTests (test, type, key, expectedError) { async = true }) + + testCommon.getMany && test('test getMany() with ' + type + ' causes error', assertAsync.ctx(function (t) { + // Add 1 assertion for every assertAsync() + t.plan(2 * 4) + + db.getMany([key], assertAsync(function (err) { + t.ok(err, 'has error') + t.ok(err instanceof Error) + t.ok(err.message.match(expectedError), 'correct error message') + })) + + db.getMany(['valid', key], assertAsync(function (err) { + t.ok(err, 'has error') + t.ok(err instanceof Error) + t.ok(err.message.match(expectedError), 'correct error message') + })) + })) } function makePutErrorTest (test, type, key, value, expectedError) { @@ -96,8 +114,8 @@ function makePutGetDelSuccessfulTest (test, testCommon, type, key, value, expect }) } -function makeErrorKeyTest (test, type, key, expectedError) { - makeGetDelErrorTests(test, type, key, expectedError) +function makeErrorKeyTest (test, testCommon, type, key, expectedError) { + makeGetDelErrorTests(test, testCommon, type, key, expectedError) makePutErrorTest(test, type, key, 'foo', expectedError) } @@ -110,11 +128,11 @@ exports.setUp = function (test, testCommon) { } exports.errorKeys = function (test, testCommon) { - makeErrorKeyTest(test, 'null key', null, /key cannot be `null` or `undefined`/) - makeErrorKeyTest(test, 'undefined key', undefined, /key cannot be `null` or `undefined`/) - makeErrorKeyTest(test, 'empty String key', '', /key cannot be an empty String/) - makeErrorKeyTest(test, 'empty Buffer key', Buffer.alloc(0), /key cannot be an empty \w*Buffer/) - makeErrorKeyTest(test, 'empty Array key', [], /key cannot be an empty Array/) + makeErrorKeyTest(test, testCommon, 'null key', null, /key cannot be `null` or `undefined`/) + makeErrorKeyTest(test, testCommon, 'undefined key', undefined, /key cannot be `null` or `undefined`/) + makeErrorKeyTest(test, testCommon, 'empty String key', '', /key cannot be an empty String/) + makeErrorKeyTest(test, testCommon, 'empty Buffer key', Buffer.alloc(0), /key cannot be an empty \w*Buffer/) + makeErrorKeyTest(test, testCommon, 'empty Array key', [], /key cannot be an empty Array/) } exports.errorValues = function (test, testCommon) { diff --git a/test/put-test.js b/test/put-test.js index ce898a5..0db8ac5 100644 --- a/test/put-test.js +++ b/test/put-test.js @@ -48,36 +48,6 @@ exports.args = function (test, testCommon) { ) t.end() }) - - testCommon.serialize && test('test _serialize object', function (t) { - t.plan(3) - const db = testCommon.factory() - db._put = function (key, value, opts, callback) { - t.ok(key) - t.ok(value) - this._nextTick(callback) - } - db.put({}, {}, function (err, val) { - t.error(err) - }) - }) - - testCommon.serialize && test('test custom _serialize*', function (t) { - t.plan(4) - const db = testCommon.factory() - db._serializeKey = db._serializeValue = function (data) { return data } - db._put = function (key, value, options, callback) { - t.deepEqual(key, { foo: 'bar' }) - t.deepEqual(value, { beep: 'boop' }) - this._nextTick(callback) - } - db.open(function () { - db.put({ foo: 'bar' }, { beep: 'boop' }, function (err) { - t.error(err) - db.close(t.error.bind(t)) - }) - }) - }) } exports.put = function (test, testCommon) { diff --git a/test/self.js b/test/self.js index 6bf632f..474a89b 100644 --- a/test/self.js +++ b/test/self.js @@ -36,6 +36,9 @@ require('./del-test').args(test, testCommon) require('./get-test').setUp(test, testCommon) require('./get-test').args(test, testCommon) +require('./get-many-test').setUp(test, testCommon) +require('./get-many-test').args(test, testCommon) + require('./put-test').setUp(test, testCommon) require('./put-test').args(test, testCommon) @@ -233,6 +236,37 @@ test('test get() extensibility', function (t) { t.end() }) +test('test getMany() extensibility', function (t) { + const spy = sinon.spy() + const expectedCb = function () {} + const expectedOptions = { asBuffer: true } + const expectedKey = 'a key' + const Test = implement(AbstractLevelDOWN, { _getMany: spy }) + const test = new Test('foobar') + + test.status = 'open' + 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.equal(spy.getCall(0).args[2], expectedCb, 'got expected 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.equal(spy.getCall(1).args[2], expectedCb, 'got expected cb argument') + t.end() +}) + test('test del() extensibility', function (t) { const spy = sinon.spy() const expectedCb = function () {} @@ -619,6 +653,44 @@ test('test clear() extensibility', function (t) { t.end() }) +test('test serialization extensibility (get)', function (t) { + t.plan(2) + + const spy = sinon.spy() + const Test = implement(AbstractLevelDOWN, { + _get: spy, + _serializeKey: function (key) { + return key.toUpperCase() + } + }) + + const test = new Test() + test.get('foo', function () {}) + + t.is(spy.callCount, 1, 'got _get() call') + t.is(spy.getCall(0).args[0], 'FOO', 'got expected key argument') +}) + +test('test serialization extensibility (getMany)', function (t) { + t.plan(2) + + const spy = sinon.spy() + const Test = implement(AbstractLevelDOWN, { + _getMany: spy, + _serializeKey: function (key) { + return key.toUpperCase() + } + }) + + const test = new Test() + + test.status = 'open' + test.getMany(['foo', 'bar'], function () {}) + + t.is(spy.callCount, 1, 'got _getMany() call') + t.same(spy.getCall(0).args[0], ['FOO', 'BAR'], 'got expected keys argument') +}) + test('test serialization extensibility (put)', function (t) { t.plan(5) @@ -909,6 +981,8 @@ test('.status', function (t) { t.end() }) }) + + t.equal(test.status, 'opening') }) t.test('open error', function (t) { diff --git a/test/util.js b/test/util.js index 5d83347..e1253c4 100644 --- a/test/util.js +++ b/test/util.js @@ -1,6 +1,7 @@ 'use strict' const nfre = /NotFound/i +const spies = [] exports.verifyNotFoundError = function verifyNotFoundError (err) { return nfre.test(err.message) || nfre.test(err.name) @@ -10,3 +11,62 @@ exports.isTypedArray = function isTypedArray (value) { return (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) || (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) } + +/** + * 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 + } +}