fixed fd leaks during tests

This commit is contained in:
Joshua Thomas Wise
2019-08-05 11:30:11 -04:00
parent aff0936044
commit 37283b8a18
8 changed files with 128 additions and 134 deletions
+2 -4
View File
@@ -1,19 +1,17 @@
'use strict';
const fs = require('fs-extra');
const path = require('path');
const { platform } = require('os');
const os = require('os');
const chai = require('chai');
const tempDir = path.join(__dirname, '..', 'temp');
const isWin = platform().startsWith('win')
let dbId = 0;
global.expect = chai.expect;
global.util = {
current: () => path.join(tempDir, `${dbId}.db`),
next: () => (++dbId, global.util.current()),
isWin,
describe: isWin ? describe.skip : describe
describeUnix: os.platform().startsWith('win') ? describe.skip : describe,
};
before(function () {
+52 -58
View File
@@ -3,31 +3,35 @@ const { existsSync } = require('fs');
const Database = require('../.');
describe('new Database()', function () {
afterEach(function () {
if (this.db) this.db.close();
});
it('should throw when given invalid argument types', function () {
expect(() => new Database('', '')).to.throw(TypeError);
expect(() => new Database({}, '')).to.throw(TypeError);
expect(() => new Database({}, {})).to.throw(TypeError);
expect(() => new Database({})).to.throw(TypeError);
expect(() => new Database(0)).to.throw(TypeError);
expect(() => new Database(123)).to.throw(TypeError);
expect(() => new Database(new String(util.next()))).to.throw(TypeError);
expect(() => new Database(() => util.next())).to.throw(TypeError);
expect(() => new Database([util.next()])).to.throw(TypeError);
expect(() => (this.db = new Database('', ''))).to.throw(TypeError);
expect(() => (this.db = new Database({}, ''))).to.throw(TypeError);
expect(() => (this.db = new Database({}, {}))).to.throw(TypeError);
expect(() => (this.db = new Database({}))).to.throw(TypeError);
expect(() => (this.db = new Database(0))).to.throw(TypeError);
expect(() => (this.db = new Database(123))).to.throw(TypeError);
expect(() => (this.db = new Database(new String(util.next())))).to.throw(TypeError);
expect(() => (this.db = new Database(() => util.next()))).to.throw(TypeError);
expect(() => (this.db = new Database([util.next()]))).to.throw(TypeError);
});
it('should throw when boolean options are provided as non-booleans', function () {
expect(() => new Database(util.next(), { readOnly: false })).to.throw(TypeError);
expect(() => new Database(util.next(), { readonly: undefined })).to.throw(TypeError);
expect(() => new Database(util.next(), { memory: undefined })).to.throw(TypeError);
expect(() => new Database(util.next(), { fileMustExist: undefined })).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { readOnly: false }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { readonly: undefined }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { memory: undefined }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { fileMustExist: undefined }))).to.throw(TypeError);
});
it('should not allow URI file paths', function () {
expect(() => new Database(`FILE:${util.next()}`)).to.throw(TypeError);
expect(() => new Database(`file:${util.next()}`)).to.throw(TypeError);
expect(() => new Database(`file:${util.next()}?mode=memory&cache=shared`)).to.throw(TypeError);
expect(() => (this.db = new Database(`FILE:${util.next()}`))).to.throw(TypeError);
expect(() => (this.db = new Database(`file:${util.next()}`))).to.throw(TypeError);
expect(() => (this.db = new Database(`file:${util.next()}?mode=memory&cache=shared`))).to.throw(TypeError);
});
it('should allow anonymous temporary databases to be created', function () {
for (const args of [[''], [], [null], [undefined], ['', { timeout: 2000 }]]) {
const db = new Database(...args);
const db = this.db = new Database(...args);
expect(db.name).to.equal('');
expect(db.memory).to.be.true;
expect(db.readonly).to.be.false;
@@ -41,123 +45,113 @@ describe('new Database()', function () {
}
});
it('should allow anonymous in-memory databases to be created', function () {
const db = new Database(':memory:');
const db = this.db = new Database(':memory:');
expect(db.name).to.equal(':memory:');
expect(db.memory).to.be.true;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(':memory:')).to.be.false;
db.close();
});
it('should allow named in-memory databases to be created', function () {
expect(existsSync(util.next())).to.be.false;
const db = new Database(util.current(), { memory: true });
const db = this.db = new Database(util.current(), { memory: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.true;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.false;
db.close();
});
it('should allow disk-bound databases to be created', function () {
expect(existsSync(util.next())).to.be.false;
const db = Database(util.current());
const db = this.db = Database(util.current());
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
db.close();
});
it('should not allow conflicting in-memory options', function () {
expect(() => new Database(':memory:', { memory: false })).to.throw(TypeError);
expect(() => new Database('', { memory: false })).to.throw(TypeError);
expect(() => (this.db = new Database(':memory:', { memory: false }))).to.throw(TypeError);
expect(() => (this.db = new Database('', { memory: false }))).to.throw(TypeError);
(new Database(':memory:', { memory: true })).close();
(new Database('', { memory: true })).close();
});
it('should allow readonly database connections to be created', function () {
expect(existsSync(util.next())).to.be.false;
expect(() => new Database(util.current(), { readonly: true })).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
expect(() => (this.db = new Database(util.current(), { readonly: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
const db = new Database(util.current(), { readonly: true });
const db = this.db = new Database(util.current(), { readonly: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.true;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
db.close();
});
it('should not allow the "readonly" option for in-memory databases', function () {
expect(existsSync(util.next())).to.be.false;
expect(() => new Database(util.current(), { memory: true, readonly: true })).to.throw(TypeError);
expect(() => new Database(':memory:', { readonly: true })).to.throw(TypeError);
expect(() => new Database('', { readonly: true })).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { memory: true, readonly: true }))).to.throw(TypeError);
expect(() => (this.db = new Database(':memory:', { readonly: true }))).to.throw(TypeError);
expect(() => (this.db = new Database('', { readonly: true }))).to.throw(TypeError);
expect(existsSync(util.current())).to.be.false;
});
it('should accept the "fileMustExist" option', function () {
expect(existsSync(util.next())).to.be.false;
expect(() => new Database(util.current(), { fileMustExist: true })).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
expect(() => (this.db = new Database(util.current(), { fileMustExist: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
const db = new Database(util.current(), { fileMustExist: true });
const db = this.db = new Database(util.current(), { fileMustExist: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
db.close();
});
it('should accept the "timeout" option', function () {
this.slow(4000); // < windows CI can be slow
this.slow(7000);
const testTimeout = (timeout) => {
const db = new Database(util.current(), { timeout });
try {
const start = Date.now();
expect(() => db.exec('BEGIN EXCLUSIVE')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_BUSY');
const end = Date.now();
expect(end - start).to.be.closeTo(timeout, util.isWin ? 1000 : 100); // < like I said, slow
expect(end - start).to.be.closeTo(timeout, 1000);
} finally {
db.close();
}
};
const blocker = new Database(util.next(), { timeout: 0x7fffffff });
const blocker = this.db = new Database(util.next(), { timeout: 0x7fffffff });
blocker.exec('BEGIN EXCLUSIVE');
testTimeout(0);
testTimeout(1000);
blocker.close();
expect(() => new Database(util.current(), { timeout: undefined })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: null })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: NaN })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: '75' })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: -1 })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: 75.01 })).to.throw(TypeError);
expect(() => new Database(util.current(), { timeout: 0x80000000 })).to.throw(RangeError);
testTimeout(3000);
blocker.close();
expect(() => (this.db = new Database(util.current(), { timeout: undefined }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: null }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: NaN }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: '75' }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: -1 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: 75.01 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: 0x80000000 }))).to.throw(RangeError);
});
it('should throw an Error if the directory does not exist', function () {
expect(existsSync(util.next())).to.be.false;
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
expect(() => new Database(filepath)).to.throw(TypeError);
expect(() => (this.db = new Database(filepath))).to.throw(TypeError);
expect(existsSync(filepath)).to.be.false;
expect(existsSync(util.current())).to.be.false;
});
it('should have a proper prototype chain', function () {
const db = new Database(util.next());
try {
expect(db).to.be.an.instanceof(Database);
expect(db.constructor).to.equal(Database);
expect(Database.prototype.constructor).to.equal(Database);
expect(Database.prototype.close).to.be.a('function');
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
} finally {
db.close();
}
const db = this.db = new Database(util.next());
expect(db).to.be.an.instanceof(Database);
expect(db.constructor).to.equal(Database);
expect(Database.prototype.constructor).to.equal(Database);
expect(Database.prototype.close).to.be.a('function');
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
});
});
+26 -23
View File
@@ -49,30 +49,33 @@ describe('Database#transaction()', function () {
});
it('should execute within an isolated transaction', function () {
const other = new Database(util.current());
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(this.db.inTransaction).to.be.false;
let ranOnce = false;
const trx = this.db.transaction((arg) => {
expect(this.db.inTransaction).to.be.true;
expect(arg).to.equal('foo');
this.db.prepare('INSERT INTO data VALUES (100)').run();
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
try {
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
ranOnce = true;
expect(this.db.inTransaction).to.be.true;
return 'bar';
});
expect(ranOnce).to.be.false;
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(this.db.inTransaction).to.be.false;
expect(trx('foo')).to.equal('bar');
expect(this.db.inTransaction).to.be.false;
expect(ranOnce).to.be.true;
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
other.close();
expect(this.db.inTransaction).to.be.false;
let ranOnce = false;
const trx = this.db.transaction((arg) => {
expect(this.db.inTransaction).to.be.true;
expect(arg).to.equal('foo');
this.db.prepare('INSERT INTO data VALUES (100)').run();
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
ranOnce = true;
expect(this.db.inTransaction).to.be.true;
return 'bar';
});
expect(ranOnce).to.be.false;
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
expect(this.db.inTransaction).to.be.false;
expect(trx('foo')).to.equal('bar');
expect(this.db.inTransaction).to.be.false;
expect(ranOnce).to.be.true;
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]);
} finally {
other.close();
}
});
it('should rollback the transaction if an exception is thrown', function () {
expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]);
+1 -3
View File
@@ -1,7 +1,7 @@
'use strict';
const Database = require('../.');
util.describe('Database#loadExtension()', function () {
util.describeUnix('Database#loadExtension()', function () {
const filepath = require('path').join(__dirname, '../build/test_extension.node');
beforeEach(function () {
this.db = new Database(util.next());
@@ -56,8 +56,6 @@ util.describe('Database#loadExtension()', function () {
} catch (err) {
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
return;
} finally {
this.db.close();
}
throw new Error('This code should not have been reached');
});
+1 -1
View File
@@ -185,7 +185,7 @@ describe('Database#backup()', function () {
await rejectsWith(SyntaxError, promise);
expect(actual).to.deep.equal(expected);
expect(existsSync(util.current())).to.be.true;
this.db.close() // < prevent leak
this.db.close();
this.db = new Database(util.current());
expect(this.db.prepare('SELECT bar FROM foo').pluck().all()).to.deep.equal([2, 8]);
});
+7 -4
View File
@@ -17,10 +17,13 @@ describe('64-bit integers', function () {
this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').bind(int, int, int).run();
const db2 = new Database(util.next());
db2.prepare('CREATE TABLE entries (a INTEGER, b REAL, c TEXT)').run();
db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int);
db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').bind(int, int, int).run();
db2.close();
try {
db2.prepare('CREATE TABLE entries (a INTEGER, b REAL, c TEXT)').run();
db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int);
db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').bind(int, int, int).run();
} finally {
db2.close();
}
});
it('should be allowed as a return value in user-defined functions', function () {
this.db.function('returnsInteger', a => Integer(a + a));
+1 -1
View File
@@ -165,7 +165,7 @@ describe('integrity checks', function () {
});
});
util.describe('Database#loadExtension()', function () {
util.describeUnix('Database#loadExtension()', function () {
const filepath = require('path').join(__dirname, '../build/test_extension.node');
specify('while iterating (blocked)', function () {
whileIterating(this, blocked(() => this.db.loadExtension(filepath)));
+38 -40
View File
@@ -3,17 +3,21 @@ const Database = require('../.');
const { Integer } = Database;
describe('verbose mode', function () {
afterEach(function () {
if (this.db) this.db.close();
});
it('should throw when not given a function or null/undefined', function () {
expect(() => new Database(util.next(), { verbose: false })).to.throw(TypeError);
expect(() => new Database(util.next(), { verbose: true })).to.throw(TypeError);
expect(() => new Database(util.next(), { verbose: 123 })).to.throw(TypeError);
expect(() => new Database(util.next(), { verbose: 'null' })).to.throw(TypeError);
expect(() => new Database(util.next(), { verbose: {} })).to.throw(TypeError);
expect(() => new Database(util.next(), { verbose: [] })).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: false }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: true }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: 123 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: 'null' }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: {} }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.next(), { verbose: [] }))).to.throw(TypeError);
});
it('should allow explicit null or undefined as a no-op', function () {
for (const verbose of [undefined, null]) {
const db = new Database(util.next(), { verbose });
const db = this.db = new Database(util.next(), { verbose });
db.exec('select 5');
db.close();
}
@@ -23,7 +27,7 @@ describe('verbose mode', function () {
function verbose(...args) {
calls.push([this, ...args]);
}
const db = new Database(util.next(), { verbose });
const db = this.db = new Database(util.next(), { verbose });
const stmt = db.prepare('select ?');
db.exec('select 5');
db.prepare('create table data (x)').run();
@@ -47,14 +51,13 @@ describe('verbose mode', function () {
[undefined, "insert into data values ('bye')"],
[undefined, 'select x from data order by rowid'],
]);
db.close();
});
it('should not fully expand very long bound parameter', function () {
let calls = [];
function verbose(...args) {
calls.push([this, ...args]);
}
const db = new Database(util.next(), { verbose });
const db = this.db = new Database(util.next(), { verbose });
const stmt = db.prepare('select ?');
stmt.get('this is a fairly short parameter');
stmt.get('this is a slightly longer parameter');
@@ -64,40 +67,35 @@ describe('verbose mode', function () {
[undefined, "select 'this is a slightly longer parame'/*+3 bytes*/"],
[undefined, "select 'this is surely a very long bound'/*+62 bytes*/"],
]);
db.close();
});
it('should abort the execution if the logger function throws', function () {
let fail = false;
let failures = 0;
const err = new Error('foo');
const db = new Database(util.next(), { verbose: () => { if (fail) throw err; } });
try {
db.prepare('create table data (x)').run();
db.function('fn', (value) => {
if (fail) failures += 1;
return value;
});
const shouldThrow = (fn) => {
expect(fn).to.not.throw();
expect(fn).to.not.throw();
fail = true;
try {
expect(fn).to.throw(err);
} finally {
fail = false;
}
expect(fn).to.not.throw();
expect(failures).to.equal(0);
};
const use = (stmt, fn) => () => fn(stmt);
shouldThrow(() => db.exec('select fn(5)'));
shouldThrow(use(db.prepare('insert into data values (fn(5))'), stmt => stmt.run()));
shouldThrow(use(db.prepare('insert into data values (fn(?))'), stmt => stmt.run(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.get(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.all(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => Array.from(stmt.iterate(5))));
} finally {
db.close()
}
const db = this.db = new Database(util.next(), { verbose: () => { if (fail) throw err; } });
db.prepare('create table data (x)').run();
db.function('fn', (value) => {
if (fail) failures += 1;
return value;
});
const shouldThrow = (fn) => {
expect(fn).to.not.throw();
expect(fn).to.not.throw();
fail = true;
try {
expect(fn).to.throw(err);
} finally {
fail = false;
}
expect(fn).to.not.throw();
expect(failures).to.equal(0);
};
const use = (stmt, fn) => () => fn(stmt);
shouldThrow(() => db.exec('select fn(5)'));
shouldThrow(use(db.prepare('insert into data values (fn(5))'), stmt => stmt.run()));
shouldThrow(use(db.prepare('insert into data values (fn(?))'), stmt => stmt.run(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.get(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.all(5)));
shouldThrow(use(db.prepare('select fn(?)'), stmt => Array.from(stmt.iterate(5))));
});
});