You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

686 lines
23 KiB

/* Copyright (c) 2012-2016 LevelUP contributors
* See list at <https://github.com/level/levelup#contributing>
* MIT License <https://github.com/level/levelup/blob/master/LICENSE.md>
*/
var levelup = require('../lib/levelup.js')
var common = require('./common')
var SlowStream = require('slow-stream')
var delayed = require('delayed')
var rimraf = require('rimraf')
var async = require('async')
var msgpack = require('msgpack-js')
var assert = require('referee').assert
var refute = require('referee').refute
var buster = require('bustermove')
var bigBlob = Array.apply(null, Array(1024 * 100)).map(function () { return 'aaaaaaaaaa' }).join('')
buster.testCase('ReadStream', {
'setUp': common.readStreamSetUp,
'tearDown': common.commonTearDown,
// TODO: test various encodings
'test simple ReadStream': function (done) {
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
}.bind(this))
}.bind(this))
},
'test pausing': function (done) {
var calls = 0
var rs
var pauseVerify = function () {
assert.equals(calls, 5, 'stream should still be paused')
rs.resume()
pauseVerify.called = true
}
var onData = function () {
if (++calls === 5) {
rs.pause()
setTimeout(pauseVerify, 50)
}
}
var verify = function () {
assert.equals(calls, this.sourceData.length, 'onData was used in test')
assert(pauseVerify.called, 'pauseVerify was used in test')
this.verify(rs, done)
}.bind(this)
this.dataSpy = this.spy(onData) // so we can still verify
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('end', verify.bind(this))
}.bind(this))
}.bind(this))
},
'test destroy() immediately': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', function () {
assert.equals(this.dataSpy.callCount, 0, '"data" event was not fired')
assert.equals(this.endSpy.callCount, 0, '"end" event was not fired')
done()
}.bind(this))
rs.destroy()
}.bind(this))
}.bind(this))
},
'test destroy() after close': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', function () {
rs.destroy()
done()
})
}.bind(this))
}.bind(this))
},
'test destroy() after closing db': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
db.close(function (err) {
refute(err)
var rs = db.createReadStream()
rs.destroy()
done()
})
})
}.bind(this))
},
'test destroy() twice': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', function () {
rs.destroy()
rs.destroy()
done()
})
})
}.bind(this))
},
'test destroy() half way through': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
var endSpy = this.spy()
var calls = 0
this.dataSpy = this.spy(function () {
if (++calls === 5) { rs.destroy() }
})
rs.on('data', this.dataSpy)
rs.on('end', endSpy)
rs.on('close', function () {
// should do "data" 5 times ONLY
assert.equals(this.dataSpy.callCount, 5, 'ReadStream emitted correct number of "data" events (5)')
this.sourceData.slice(0, 5).forEach(function (d, i) {
var call = this.dataSpy.getCall(i)
assert(call)
if (call) {
assert.equals(call.args.length, 1, 'ReadStream "data" event #' + i + ' fired with 1 argument')
refute.isNull(call.args[0].key, 'ReadStream "data" event #' + i + ' argument has "key" property')
refute.isNull(call.args[0].value, 'ReadStream "data" event #' + i + ' argument has "value" property')
assert.equals(call.args[0].key, d.key, 'ReadStream "data" event #' + i + ' argument has correct "key"')
assert.equals(+call.args[0].value, +d.value, 'ReadStream "data" event #' + i + ' argument has correct "value"')
}
}.bind(this))
done()
}.bind(this))
}.bind(this))
}.bind(this))
},
'test readStream() with "reverse=true"': function (done) {
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ reverse: true })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData.reverse() // for verify
}.bind(this))
}.bind(this))
},
'test readStream() with "start"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: '50' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
this.sourceData = this.sourceData.slice(50)
}.bind(this))
}.bind(this))
},
'test readStream() with "start" and "reverse=true"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: '50', reverse: true })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// reverse and slice off the first 50 so verify() expects only the first 50 even though all 100 are in the db
this.sourceData.reverse()
this.sourceData = this.sourceData.slice(49)
}.bind(this))
}.bind(this))
},
'test readStream() with "start" being mid-way key (float)': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
// '49.5' doesn't actually exist but we expect it to start at '50' because '49' < '49.5' < '50' (in string terms as well as numeric)
var rs = db.createReadStream({ start: '49.5' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
this.sourceData = this.sourceData.slice(50)
}.bind(this))
}.bind(this))
},
'test readStream() with "start" being mid-way key (float) and "reverse=true"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: '49.5', reverse: true })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// reverse & slice off the first 50 so verify() expects only the first 50 even though all 100 are in the db
this.sourceData.reverse()
this.sourceData = this.sourceData.slice(50)
}.bind(this))
}.bind(this))
},
'test readStream() with "start" being mid-way key (string)': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
// '499999' doesn't actually exist but we expect it to start at '50' because '49' < '499999' < '50' (in string terms)
// the same as the previous test but we're relying solely on string ordering
var rs = db.createReadStream({ start: '499999' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
this.sourceData = this.sourceData.slice(50)
}.bind(this))
}.bind(this))
},
'test readStream() with "end"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '50' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
this.sourceData = this.sourceData.slice(0, 51)
}.bind(this))
}.bind(this))
},
'test readStream() with "end" being mid-way key (float)': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '50.5' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
this.sourceData = this.sourceData.slice(0, 51)
}.bind(this))
}.bind(this))
},
'test readStream() with "end" being mid-way key (string)': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '50555555' })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
this.sourceData = this.sourceData.slice(0, 51)
}.bind(this))
}.bind(this))
},
'test readStream() with "end" being mid-way key (float) and "reverse=true"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '50.5', reverse: true })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData.reverse()
this.sourceData = this.sourceData.slice(0, 49)
}.bind(this))
}.bind(this))
},
'test readStream() with both "start" and "end"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: 30, end: 70 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// should include 30 to 70, inclusive
this.sourceData = this.sourceData.slice(30, 71)
}.bind(this))
}.bind(this))
},
'test readStream() with both "start" and "end" and "reverse=true"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: 70, end: 30, reverse: true })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
// expect 70 -> 30 inclusive
this.sourceData.reverse()
this.sourceData = this.sourceData.slice(29, 70)
}.bind(this))
}.bind(this))
},
'test hex encoding': function (done) {
var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'hex' }
var data = [
{ type: 'put', key: 'ab', value: 'abcdef0123456789' }
]
this.openTestDatabase({}, function (db) {
db.batch(data.slice(), options, function (err) {
refute(err)
var rs = db.createReadStream(options)
rs.on('data', function (data) {
assert.equals(data.value, 'abcdef0123456789')
})
rs.on('end', this.endSpy)
rs.on('close', done)
}.bind(this))
}.bind(this))
},
'test json encoding': function (done) {
var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
var data = [
{ type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } },
{ type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } },
{ type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
]
this.openTestDatabase(options, function (db) {
db.batch(data.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done, data))
}.bind(this))
}.bind(this))
},
'test injectable encoding': function (done) {
var options = {
createIfMissing: true,
errorIfExists: true,
keyEncoding: 'utf8',
valueEncoding: {
decode: msgpack.decode,
encode: msgpack.encode,
buffer: true
}}
var data = [
{ type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } },
{ type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } },
{ type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } },
{ type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } },
{ type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
]
this.openTestDatabase(options, function (db) {
db.batch(data.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done, data))
}.bind(this))
}.bind(this))
},
'test readStream() "reverse=true" not sticky (issue #6)': function (done) {
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
// read in reverse, assume all's good
var rs = db.createReadStream({ reverse: true })
rs.on('close', function () {
// now try reading the other way
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
}.bind(this))
rs.resume()
}.bind(this))
}.bind(this))
},
'test ReadStream, start=0': function (done) {
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: 0 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
}.bind(this))
}.bind(this))
},
// we don't expect any data to come out of here because the keys start at '00' not 0
// we just want to ensure that we don't kill the process
'test ReadStream, end=0': function (done) {
this.openTestDatabase(function (db) {
// execute
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: 0 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData = []
}.bind(this))
}.bind(this))
},
// ok, so here's the deal, this is kind of obscure: when you have 2 databases open and
// have a readstream coming out from both of them with no references to the dbs left
// V8 will GC one of them and you'll get an failed assert from leveldb.
// This ISN'T a problem if you only have one of them open, even if the db gets GCed!
// Process:
// * open
// * batch write data
// * close
// * reopen
// * create ReadStream, keeping no reference to the db
// * pipe ReadStream through SlowStream just to make sure GC happens
// - the error should occur here if the bug exists
// * when both streams finish, verify all 'data' events happened
'test ReadStream without db ref doesn\'t get GCed': function (done) {
var dataSpy1 = this.spy()
var dataSpy2 = this.spy()
var location1 = common.nextLocation()
var location2 = common.nextLocation()
var sourceData = this.sourceData
var verify = function () {
// no reference to `db` here, should have been GCed by now if it could be
assert(dataSpy1.callCount, sourceData.length)
assert(dataSpy2.callCount, sourceData.length)
async.parallel([ rimraf.bind(null, location1), rimraf.bind(null, location2) ], done)
}
var execute = function (d, callback) {
// no reference to `db` here, could be GCed
d.readStream
.pipe(new SlowStream({ maxWriteInterval: 5 }))
.on('data', d.spy)
.on('close', delayed.delayed(callback, 0.05))
}
var open = function (reopen, location, callback) {
levelup(location, { createIfMissing: !reopen, errorIfExists: !reopen }, callback)
}
var write = function (db, callback) { db.batch(sourceData.slice(), callback) }
var close = function (db, callback) { db.close(callback) }
var setup = function (callback) {
async.map([ location1, location2 ], open.bind(null, false), function (err, dbs) {
refute(err)
if (err) return
async.map(dbs, write, function (err) {
refute(err)
if (err) return
async.forEach(dbs, close, callback)
})
})
}
var reopen = function () {
async.map([ location1, location2 ], open.bind(null, true), function (err, dbs) {
refute(err)
if (err) return
async.forEach([
{ readStream: dbs[0].createReadStream(), spy: dataSpy1 },
{ readStream: dbs[1].createReadStream(), spy: dataSpy2 }
], execute, verify)
})
}
setup(delayed.delayed(reopen, 0.05))
},
// this is just a fancy way of testing levelup('/path').createReadStream()
// i.e. not waiting for 'open' to complete
// the logic for this is inside the ReadStream constructor which waits for 'ready'
'test ReadStream on pre-opened db': function (done) {
var execute = function (db) {
// is in limbo
refute(db.isOpen())
refute(db.isClosed())
var rs = db.createReadStream()
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
}.bind(this)
var setup = function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
db.close(function (err) {
refute(err)
var db2 = levelup(db.location, { createIfMissing: false, errorIfExists: false, valueEncoding: 'utf8' })
execute(db2)
})
})
}.bind(this)
this.openTestDatabase(setup)
},
'test readStream() with "limit"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ limit: 20 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData = this.sourceData.slice(0, 20)
}.bind(this))
}.bind(this))
},
'test readStream() with "start" and "limit"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ start: '20', limit: 20 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData = this.sourceData.slice(20, 40)
}.bind(this))
}.bind(this))
},
'test readStream() with "end" after "limit"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '50', limit: 20 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData = this.sourceData.slice(0, 20)
}.bind(this))
}.bind(this))
},
'test readStream() with "end" before "limit"': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream({ end: '30', limit: 50 })
rs.on('data', this.dataSpy)
rs.on('end', this.endSpy)
rs.on('close', this.verify.bind(this, rs, done))
this.sourceData = this.sourceData.slice(0, 31)
}.bind(this))
}.bind(this))
},
// can, fairly reliably, trigger a core dump if next/end isn't
// protected properly
// the use of large blobs means that next() takes time to return
// so we should be able to slip in an end() while it's working
'test iterator next/end race condition': function (done) {
var data = []
var i = 5
var v
while (i--) {
v = bigBlob + i
data.push({ type: 'put', key: v, value: v })
}
this.openTestDatabase(function (db) {
db.batch(data, function (err) {
refute(!!err)
var rs = db.createReadStream().on('close', done)
rs.once('data', rs.destroy.bind(rs))
})
})
},
'test can only end once': function (done) {
this.openTestDatabase(function (db) {
db.batch(this.sourceData.slice(), function (err) {
refute(err)
var rs = db.createReadStream()
.on('close', done)
process.nextTick(function () {
rs.destroy()
})
})
}.bind(this))
}
})