node-persistent-queue
Version:
Simple SQLite backed Queue for running many short tasks in Node.js event thread
603 lines (508 loc) • 11.9 kB
JavaScript
/**
* test.js
*
* Mocha Test Script
*
* node-persistent-queue
*
* 18/05/2019
*
* Copyright (C) 2019 Damien Clark (damo.clarky@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint no-undef: 0 */
const debug = false ;
// eslint-disable-next-line no-unused-vars
const should = require('should') ;
const sinon = require('sinon') ;
const os = require('os') ;
const fs = require('fs') ;
const path = require('path') ;
require('should-sinon') ;
const Queue = require('../index') ;
describe('Calling Constructor', () => {
it('should use :memory: if file is empty string', done => {
let q = new Queue('') ;
q.open().should.be.fulfilled() ;
done() ;
}) ;
it('should throw if filename not provided', done => {
(() =>{
new Queue() ;
}).should.throw(Error) ;
done() ;
}) ;
it('should throw when passed a batchSize less than 1', () => {
(() => {
new Queue(':memory:', -1) ;
}).should.throw(Error) ;
}) ;
it('should throw when passed a batchSize that is not a number', () => {
(() => {
new Queue(':memory:', 'text') ;
}).should.throw(Error) ;
}) ;
}) ;
describe('Correct queue fifo order', () => {
let q ;
before(done => {
// Remove previous db3.sqlite (if exists) before creating db anew
fs.unlink('./test/db3.sqlite', () => {
q = new Queue('./test/db3.sqlite') ;
done() ;
}) ;
}) ;
it('should execute jobs in fifo order', done => {
let sequence = 0 ;
q.on('next', task => {
task.job.sequence.should.equal(sequence++) ;
q.done() ;
}) ;
q.on('empty', () => {
q.close() ;
done() ;
}) ;
q.open()
.then(() => {
q.start() ;
for(let i = 0 ; i < 1000 ; ++i) {
let task = {sequence: i} ;
q.add(task) ;
}
}) ;
}) ;
}) ;
describe('Search remaining jobs', () => {
let q ;
beforeEach(done => {
q = new Queue(':memory:', 10) ;
q.open()
.then(() => done())
.catch(err => done(err)) ;
}) ;
it('should find first job in the queue', done => {
q.open()
.then(() => {
let promises = [] ;
for(let i = 1 ; i <= 1000 ; ++i) {
let task = {sequence: i % 501} ;
promises.push(q.add(task)) ;
}
// Wait for all tasks to be added before calling hasJob method to search for it
Promise.all(promises)
.then(() => {
for(let i = 1 ; i <= 500 ; ++i)
q.getFirstJobId({sequence: i}).should.be.fulfilledWith(i) ;
q.close().then(() => done()) ;
})
.catch(err => console.log(err)) ;
}) ;
}) ;
it('should find first job in the in-memory queue', done => {
q.open()
.then(() => {
let promises = [] ;
promises.push(q.add({})) ;
for(let i = 1 ; i <= 1000 ; ++i) {
let task = {sequence: i % 501} ;
promises.push(q.add(task)) ;
}
// Grab first job and throw away so in-memory queue is hydrated
q.on('next', () => {
q.stop() ;
q.done() ;
// Now let's check if all items are
for(let i = 1 ; i <= 500 ; ++i)
q.getFirstJobId({sequence: i}).should.be.fulfilledWith(i+1) ;
q.close().then(() => done()) ;
}) ;
// Wait for all tasks to be added before calling hasJob method to search for it
Promise.all(promises)
.then(() =>{
q.start() ;
})
.catch(err =>{
console.log(err) ;
}) ;
}) ;
}) ;
it('should find all matching jobs in the queue and in order', done => {
q.open()
.then(() => {
let promises = [] ;
for(let i = 1 ; i <= 10 ; ++i) {
let task = {sequence: i % 5} ;
promises.push(q.add(task)) ;
}
// Wait for all tasks to be added before calling hasJob method to search for it
Promise.all(promises)
.then(() =>{
for(let i = 1 ; i <= 5 ; ++i)
q.getJobIds({sequence: i % 5}).should.be.fulfilledWith([i, i + 5]) ;
q.close().then(() =>{
done() ;
}) ;
}) ;
}) ;
}) ;
it('should return empty array if job not in queue', done => {
q.open()
.then(() => {
let promises = [] ;
for(let i = 1 ; i <= 10 ; ++i) {
let task = {sequence: i} ;
promises.push(q.add(task)) ;
}
// Wait for all tasks to be added before calling hasJob method to search for it
Promise.all(promises)
.then(() =>{
for(let i = 1 ; i <= 5 ; ++i)
q.getJobIds({sequence: 100}).should.be.fulfilledWith([]) ;
q.close().then(() =>{
done() ;
}) ;
}) ;
}) ;
}) ;
it('should return null if job not in queue', done => {
q.open()
.then(() => {
let promises = [] ;
for(let i = 1 ; i <= 10 ; ++i) {
let task = {sequence: i} ;
promises.push(q.add(task)) ;
}
// Wait for all tasks to be added before calling hasJob method to search for it
Promise.all(promises)
.then(() =>{
for(let i = 1 ; i <= 5 ; ++i)
q.getFirstJobId({sequence: 100}).should.be.fulfilledWith(null) ;
q.close().then(() =>{
done() ;
}) ;
}) ;
}) ;
}) ;
}) ;
describe('Unopened SQLite DB', () => {
let q = new Queue(':memory:', 2) ;
it('should throw on calling start() before open is called', () => {
(() => {
q.start() ;
}).should.throw(Error) ;
}) ;
it('should throw on calling isEmpty() before open is called', () => {
(() => {
q.isEmpty() ;
}).should.throw(Error) ;
}) ;
it('should throw on calling getSqlite3() before open is called', () => {
(() => {
q.getSqlite3() ;
}).should.throw(Error) ;
}) ;
}) ;
describe('Open Errors', () => {
it('should reject Promise on no write permissions to db filename', done => {
let q = new Queue('/cantwritetome', 2) ;
q.open().should.be.rejected() ;
done() ;
}) ;
it('should reject Promise when db filename is not a string', done => {
let q = new Queue(true, 2) ;
q.open().should.be.rejected() ;
done() ;
}) ;
}) ;
describe('Maintaining queue length count', () => {
it('should count existing jobs in db on open', done => {
let q = new Queue('./test/db2.sqlite') ;
q.open()
.then(() => {
q.getLength().should.equal(1) ;
return q.close() ;
})
.then(() => {
done() ;
})
.catch(err => {
done(err) ;
}) ;
}) ;
it('should count jobs as added and completed', done => {
let tmpdb = os.tmpdir() + path.sep + process.pid + '.sqlite' ;
let q = new Queue(tmpdb) ;
/**
* Count jobs
* @type {number}
*/
let c = 0 ;
q.on('add', () => {
q.getLength().should.equal(++c) ;
}) ;
q.open()
.then(() => {
q.add('1') ;
q.add('2') ;
q.add('3') ;
return q.close() ;
})
.then(() => {
q = new Queue(tmpdb) ;
return q.open() ;
})
.then(() => {
q.getLength().should.equal(3) ;
q.on('next', () => {
q.getLength().should.equal(c--) ;
q.done() ;
}) ;
q.on('empty', () => {
q.getLength().should.equal(0) ;
q.close()
.then(() => {
fs.unlinkSync(tmpdb) ;
done() ;
}) ;
}) ;
q.start() ;
})
.catch(err => {
done(err) ;
}) ;
}) ;
}) ;
describe('Close Errors', () => {
let q = new Queue(':memory:') ;
before(done => {
q.open()
.then(() => {
done() ;
}) ;
}) ;
it('should close properly', done => {
q.add('1') ;
q.close().should.be.fulfilled() ;
done() ;
}) ;
}) ;
describe('Invalid JSON', () => {
it('should throw on bad json stored in db', done => {
let q = new Queue('./test/db.sqlite', 1) ;
q.open()
.should.be.rejectedWith(SyntaxError) ;
done() ;
}) ;
}) ;
describe('Emitters', () => {
let q ;
beforeEach(done => {
q = new Queue(':memory:') ;
q.open()
.then(() => {
done() ;
})
.catch(err => {
done(err) ;
}) ;
}) ;
afterEach(done => {
q.close()
.then(() =>{
done() ;
})
.catch(err => {
done(err) ;
}) ;
}) ;
it('should emit add', done => {
q.on('add', job => {
job.job.should.equal('1') ;
done() ;
}) ;
q.add('1') ;
}) ;
it('should emit start', done => {
let s = sinon.spy() ;
q.on('start', s) ;
q.start() ;
s.should.be.calledOnce() ;
q.isStarted().should.be.equal(true) ;
done() ;
}) ;
it('should emit next when adding after start', done => {
q.on('next', job => {
job.job.should.equal('1') ;
// TODO: q.done() ;
q.done() ;
done() ;
}) ;
q.start() ;
q.add('1') ;
}) ;
it('should emit next when adding before start', done => {
q.on('next', job => {
job.job.should.equal('1') ;
q.done() ;
done() ;
}) ;
q.add('1') ;
q.start() ;
}) ;
it('should emit empty', done => {
let empty = 0 ;
q.on('empty', () =>{
// empty should only emit once
(++empty).should.be.equal(1) ;
q.getLength().should.equal(0) ;
done() ;
}) ;
q.on('next', job => {
if(debug) console.log(job) ;
q.done() ;
}) ;
q.add('1') ;
q.add('2') ;
q.start() ;
}) ;
it('3 adds before start should emit 3 nexts', done => {
let next = 0 ;
q.on('empty', () =>{
next.should.be.equal(3) ;
q.getLength().should.equal(0) ;
done() ;
}) ;
q.on('next', job => {
if(debug) console.log(job) ;
++next ;
q.done() ;
}) ;
q.add('1') ;
q.add('2') ;
q.add('3') ;
q.start() ;
}) ;
it('should add 3 jobs and after start should emit 3 nexts', done => {
let next = 0 ;
q.on('empty', () =>{
next.should.be.equal(3) ;
q.getLength().should.equal(0) ;
done() ;
}) ;
q.on('next', job => {
if(debug) console.log(job) ;
++next ;
q.done() ;
}) ;
q.start() ;
q.add('1') ;
q.add('2') ;
q.add('3') ;
}) ;
it('should start in middle of 3 adds and should emit 3 nexts', done => {
let next = 0 ;
q.on('empty', () =>{
next.should.be.equal(3) ;
q.getLength().should.equal(0) ;
done() ;
}) ;
q.on('next', job => {
if(debug) console.log(job) ;
++next ;
q.done() ;
}) ;
q.add('1') ;
q.add('2') ;
q.start() ;
q.add('3') ;
}) ;
it('should emit stop', done => {
let stop = 0 ;
q.on('stop', () =>{
(++stop).should.be.equal(1) ;
q.isStarted().should.be.equal(false) ;
done() ;
}) ;
q.on('empty', () =>{
q.stop() ;
}) ;
q.on('next', job => {
if(debug) console.log(job) ;
q.done() ;
}) ;
q.add('1') ;
q.add('2') ;
q.start() ;
q.add('3') ;
q.add('4') ;
}) ;
it('should emit open', done => {
let q1 = new Queue(':memory:') ;
let open = 0 ;
q1.on('open', () => {
(++open).should.be.equal(1) ;
q1.isOpen().should.be.equal(true) ;
q1.close()
.then(() => {
done() ;
}) ;
}) ;
q1.open() ;
}) ;
it('should emit close', done => {
let q1 = new Queue(':memory:') ;
let close = 0 ;
q1.on('close', () => {
(++close).should.be.equal(1) ;
q1.isOpen().should.be.equal(false) ;
}) ;
q1.open()
.then(() => {
return q1.close() ;
})
.then(() => {
done() ;
}) ;
}) ;
}) ;
describe('Deleting jobs', () => {
let q ;
beforeEach(done => {
q = new Queue(':memory:') ;
q.open()
.then(() => done())
.catch(err => done(err)) ;
}) ;
it('should allow the user to delete a yet-to-be-processed job', done => {
q.open()
.then(() => {
let addPromises = [] ;
for(let i = 0 ; i < 10 ; ++i) {
let task = {sequence: i % 501} ;
addPromises.push(q.add(task)) ;
}
let deletePromises = [] ;
Promise.all(addPromises).then((jobIds) => {
for(let i = 0 ; i < 10 ; ++i) {
(typeof jobIds[i]).should.equal('number') ;
deletePromises.push(q.delete(jobIds[i])) ;
}
return Promise.all(deletePromises).then(() => {
q.getLength().should.equal(0) ;
q.close().then(() => done()) ;
}) ;
}) ;
}) ;
}) ;
}) ;