var should = require ( 'chai' ) . should ( )
, assert = require ( 'chai' ) . assert
, testDb = 'workspace/test.db'
, fs = require ( 'fs' )
, path = require ( 'path' )
, _ = require ( 'underscore' )
, async = require ( 'async' )
, model = require ( '../lib/model' )
, customUtils = require ( '../lib/customUtils' )
, Datastore = require ( '../lib/datastore' )
, Persistence = require ( '../lib/persistence' )
, storage = require ( '../lib/storage' )
, child _process = require ( 'child_process' )
;
describe ( 'Persistence' , function ( ) {
var d ;
beforeEach ( function ( done ) {
d = new Datastore ( { filename : testDb } ) ;
d . filename . should . equal ( testDb ) ;
d . inMemoryOnly . should . equal ( false ) ;
async . waterfall ( [
function ( cb ) {
Persistence . ensureDirectoryExists ( path . dirname ( testDb ) , function ( ) {
fs . exists ( testDb , function ( exists ) {
if ( exists ) {
fs . unlink ( testDb , cb ) ;
} else { return cb ( ) ; }
} ) ;
} ) ;
}
, function ( cb ) {
d . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
d . getAllData ( ) . length . should . equal ( 0 ) ;
return cb ( ) ;
} ) ;
}
] , done ) ;
} ) ;
it ( 'Every line represents a document' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { _id : "2" , hello : 'world' } ) + '\n' +
model . serialize ( { _id : "3" , nested : { today : now } } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 3 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "2" , hello : 'world' } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 2 ] , { _id : "3" , nested : { today : now } } ) . should . equal ( true ) ;
} ) ;
it ( 'Badly formatted lines have no impact on the treated data' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
'garbage\n' +
model . serialize ( { _id : "3" , nested : { today : now } } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "3" , nested : { today : now } } ) . should . equal ( true ) ;
} ) ;
it ( 'Well formatted lines that have no _id are not included in the data' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { _id : "2" , hello : 'world' } ) + '\n' +
model . serialize ( { nested : { today : now } } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "2" , hello : 'world' } ) . should . equal ( true ) ;
} ) ;
it ( 'If two lines concern the same doc (= same _id), the last one is the good version' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { _id : "2" , hello : 'world' } ) + '\n' +
model . serialize ( { _id : "1" , nested : { today : now } } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , nested : { today : now } } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "2" , hello : 'world' } ) . should . equal ( true ) ;
} ) ;
it ( 'If a doc contains $$deleted: true, that means we need to remove it from the data' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { _id : "2" , hello : 'world' } ) + '\n' +
model . serialize ( { _id : "1" , $$deleted : true } ) + '\n' +
model . serialize ( { _id : "3" , today : now } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "2" , hello : 'world' } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "3" , today : now } ) . should . equal ( true ) ;
} ) ;
it ( 'If a doc contains $$deleted: true, no error is thrown if the doc wasnt in the list before' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { _id : "2" , $$deleted : true } ) + '\n' +
model . serialize ( { _id : "3" , today : now } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "3" , today : now } ) . should . equal ( true ) ;
} ) ;
it ( 'If a doc contains $$indexCreated, no error is thrown during treatRawData and we can get the index options' , function ( ) {
var now = new Date ( )
, rawData = model . serialize ( { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) + '\n' +
model . serialize ( { $$indexCreated : { fieldName : "test" , unique : true } } ) + '\n' +
model . serialize ( { _id : "3" , today : now } )
, treatedData = d . persistence . treatRawData ( rawData ) . data
, indexes = d . persistence . treatRawData ( rawData ) . indexes
;
Object . keys ( indexes ) . length . should . equal ( 1 ) ;
assert . deepEqual ( indexes . test , { fieldName : "test" , unique : true } ) ;
treatedData . sort ( function ( a , b ) { return a . _id - b . _id ; } ) ;
treatedData . length . should . equal ( 2 ) ;
_ . isEqual ( treatedData [ 0 ] , { _id : "1" , a : 2 , ages : [ 1 , 5 , 12 ] } ) . should . equal ( true ) ;
_ . isEqual ( treatedData [ 1 ] , { _id : "3" , today : now } ) . should . equal ( true ) ;
} ) ;
it ( 'Compact database on load' , function ( done ) {
d . insert ( { a : 2 } , function ( ) {
d . insert ( { a : 4 } , function ( ) {
d . remove ( { a : 2 } , { } , function ( ) {
// Here, the underlying file is 3 lines long for only one document
var data = fs . readFileSync ( d . filename , 'utf8' ) . split ( '\n' )
, filledCount = 0 ;
data . forEach ( function ( item ) { if ( item . length > 0 ) { filledCount += 1 ; } } ) ;
filledCount . should . equal ( 3 ) ;
d . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
// Now, the file has been compacted and is only 1 line long
var data = fs . readFileSync ( d . filename , 'utf8' ) . split ( '\n' )
, filledCount = 0 ;
data . forEach ( function ( item ) { if ( item . length > 0 ) { filledCount += 1 ; } } ) ;
filledCount . should . equal ( 1 ) ;
done ( ) ;
} ) ;
} )
} ) ;
} ) ;
} ) ;
it ( 'Calling loadDatabase after the data was modified doesnt change its contents' , function ( done ) {
d . loadDatabase ( function ( ) {
d . insert ( { a : 1 } , function ( err ) {
assert . isNull ( err ) ;
d . insert ( { a : 2 } , function ( err ) {
var data = d . getAllData ( )
, doc1 = _ . find ( data , function ( doc ) { return doc . a === 1 ; } )
, doc2 = _ . find ( data , function ( doc ) { return doc . a === 2 ; } )
;
assert . isNull ( err ) ;
data . length . should . equal ( 2 ) ;
doc1 . a . should . equal ( 1 ) ;
doc2 . a . should . equal ( 2 ) ;
d . loadDatabase ( function ( err ) {
var data = d . getAllData ( )
, doc1 = _ . find ( data , function ( doc ) { return doc . a === 1 ; } )
, doc2 = _ . find ( data , function ( doc ) { return doc . a === 2 ; } )
;
assert . isNull ( err ) ;
data . length . should . equal ( 2 ) ;
doc1 . a . should . equal ( 1 ) ;
doc2 . a . should . equal ( 2 ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'Calling loadDatabase after the datafile was removed will reset the database' , function ( done ) {
d . loadDatabase ( function ( ) {
d . insert ( { a : 1 } , function ( err ) {
assert . isNull ( err ) ;
d . insert ( { a : 2 } , function ( err ) {
var data = d . getAllData ( )
, doc1 = _ . find ( data , function ( doc ) { return doc . a === 1 ; } )
, doc2 = _ . find ( data , function ( doc ) { return doc . a === 2 ; } )
;
assert . isNull ( err ) ;
data . length . should . equal ( 2 ) ;
doc1 . a . should . equal ( 1 ) ;
doc2 . a . should . equal ( 2 ) ;
fs . unlink ( testDb , function ( err ) {
assert . isNull ( err ) ;
d . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
d . getAllData ( ) . length . should . equal ( 0 ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'Calling loadDatabase after the datafile was modified loads the new data' , function ( done ) {
d . loadDatabase ( function ( ) {
d . insert ( { a : 1 } , function ( err ) {
assert . isNull ( err ) ;
d . insert ( { a : 2 } , function ( err ) {
var data = d . getAllData ( )
, doc1 = _ . find ( data , function ( doc ) { return doc . a === 1 ; } )
, doc2 = _ . find ( data , function ( doc ) { return doc . a === 2 ; } )
;
assert . isNull ( err ) ;
data . length . should . equal ( 2 ) ;
doc1 . a . should . equal ( 1 ) ;
doc2 . a . should . equal ( 2 ) ;
fs . writeFile ( testDb , '{"a":3,"_id":"aaa"}' , 'utf8' , function ( err ) {
assert . isNull ( err ) ;
d . loadDatabase ( function ( err ) {
var data = d . getAllData ( )
, doc1 = _ . find ( data , function ( doc ) { return doc . a === 1 ; } )
, doc2 = _ . find ( data , function ( doc ) { return doc . a === 2 ; } )
, doc3 = _ . find ( data , function ( doc ) { return doc . a === 3 ; } )
;
assert . isNull ( err ) ;
data . length . should . equal ( 1 ) ;
doc3 . a . should . equal ( 3 ) ;
assert . isUndefined ( doc1 ) ;
assert . isUndefined ( doc2 ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( "When treating raw data, refuse to proceed if too much data is corrupt, to avoid data loss" , function ( done ) {
var corruptTestFilename = 'workspace/corruptTest.db'
, fakeData = '{"_id":"one","hello":"world"}\n' + 'Some corrupt data\n' + '{"_id":"two","hello":"earth"}\n' + '{"_id":"three","hello":"you"}\n'
, d
;
fs . writeFileSync ( corruptTestFilename , fakeData , "utf8" ) ;
// Default corruptAlertThreshold
d = new Datastore ( { filename : corruptTestFilename } ) ;
d . loadDatabase ( function ( err ) {
assert . isDefined ( err ) ;
assert . isNotNull ( err ) ;
fs . writeFileSync ( corruptTestFilename , fakeData , "utf8" ) ;
d = new Datastore ( { filename : corruptTestFilename , corruptAlertThreshold : 1 } ) ;
d . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
fs . writeFileSync ( corruptTestFilename , fakeData , "utf8" ) ;
d = new Datastore ( { filename : corruptTestFilename , corruptAlertThreshold : 0 } ) ;
d . loadDatabase ( function ( err ) {
assert . isDefined ( err ) ;
assert . isNotNull ( err ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
describe ( 'Serialization hooks' , function ( ) {
var as = function ( s ) { return "before_" + s + "_after" ; }
, bd = function ( s ) { return s . substring ( 7 , s . length - 6 ) ; }
it ( "Declaring only one hook will throw an exception to prevent data loss" , function ( done ) {
var hookTestFilename = 'workspace/hookTest.db'
storage . ensureFileDoesntExist ( hookTestFilename , function ( ) {
fs . writeFileSync ( hookTestFilename , "Some content" , "utf8" ) ;
( function ( ) {
new Datastore ( { filename : hookTestFilename , autoload : true
, afterSerialization : as
} ) ;
} ) . should . throw ( ) ;
// Data file left untouched
fs . readFileSync ( hookTestFilename , "utf8" ) . should . equal ( "Some content" ) ;
( function ( ) {
new Datastore ( { filename : hookTestFilename , autoload : true
, beforeDeserialization : bd
} ) ;
} ) . should . throw ( ) ;
// Data file left untouched
fs . readFileSync ( hookTestFilename , "utf8" ) . should . equal ( "Some content" ) ;
done ( ) ;
} ) ;
} ) ;
it ( "Declaring two hooks that are not reverse of one another will cause an exception to prevent data loss" , function ( done ) {
var hookTestFilename = 'workspace/hookTest.db'
storage . ensureFileDoesntExist ( hookTestFilename , function ( ) {
fs . writeFileSync ( hookTestFilename , "Some content" , "utf8" ) ;
( function ( ) {
new Datastore ( { filename : hookTestFilename , autoload : true
, afterSerialization : as
, beforeDeserialization : function ( s ) { return s ; }
} ) ;
} ) . should . throw ( ) ;
// Data file left untouched
fs . readFileSync ( hookTestFilename , "utf8" ) . should . equal ( "Some content" ) ;
done ( ) ;
} ) ;
} ) ;
it ( "A serialization hook can be used to transform data before writing new state to disk" , function ( done ) {
var hookTestFilename = 'workspace/hookTest.db'
storage . ensureFileDoesntExist ( hookTestFilename , function ( ) {
var d = new Datastore ( { filename : hookTestFilename , autoload : true
, afterSerialization : as
, beforeDeserialization : bd
} )
;
d . insert ( { hello : "world" } , function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
, doc0 = bd ( data [ 0 ] )
;
data . length . should . equal ( 2 ) ;
data [ 0 ] . substring ( 0 , 7 ) . should . equal ( 'before_' ) ;
data [ 0 ] . substring ( data [ 0 ] . length - 6 ) . should . equal ( '_after' ) ;
doc0 = model . deserialize ( doc0 ) ;
Object . keys ( doc0 ) . length . should . equal ( 2 ) ;
doc0 . hello . should . equal ( 'world' ) ;
d . insert ( { p : 'Mars' } , function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
, doc0 = bd ( data [ 0 ] )
, doc1 = bd ( data [ 1 ] )
;
data . length . should . equal ( 3 ) ;
data [ 0 ] . substring ( 0 , 7 ) . should . equal ( 'before_' ) ;
data [ 0 ] . substring ( data [ 0 ] . length - 6 ) . should . equal ( '_after' ) ;
data [ 1 ] . substring ( 0 , 7 ) . should . equal ( 'before_' ) ;
data [ 1 ] . substring ( data [ 1 ] . length - 6 ) . should . equal ( '_after' ) ;
doc0 = model . deserialize ( doc0 ) ;
Object . keys ( doc0 ) . length . should . equal ( 2 ) ;
doc0 . hello . should . equal ( 'world' ) ;
doc1 = model . deserialize ( doc1 ) ;
Object . keys ( doc1 ) . length . should . equal ( 2 ) ;
doc1 . p . should . equal ( 'Mars' ) ;
d . ensureIndex ( { fieldName : 'idefix' } , function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
, doc0 = bd ( data [ 0 ] )
, doc1 = bd ( data [ 1 ] )
, idx = bd ( data [ 2 ] )
;
data . length . should . equal ( 4 ) ;
data [ 0 ] . substring ( 0 , 7 ) . should . equal ( 'before_' ) ;
data [ 0 ] . substring ( data [ 0 ] . length - 6 ) . should . equal ( '_after' ) ;
data [ 1 ] . substring ( 0 , 7 ) . should . equal ( 'before_' ) ;
data [ 1 ] . substring ( data [ 1 ] . length - 6 ) . should . equal ( '_after' ) ;
doc0 = model . deserialize ( doc0 ) ;
Object . keys ( doc0 ) . length . should . equal ( 2 ) ;
doc0 . hello . should . equal ( 'world' ) ;
doc1 = model . deserialize ( doc1 ) ;
Object . keys ( doc1 ) . length . should . equal ( 2 ) ;
doc1 . p . should . equal ( 'Mars' ) ;
idx = model . deserialize ( idx ) ;
assert . deepEqual ( idx , { '$$indexCreated' : { fieldName : 'idefix' } } ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( "Use serialization hook when persisting cached database or compacting" , function ( done ) {
var hookTestFilename = 'workspace/hookTest.db'
storage . ensureFileDoesntExist ( hookTestFilename , function ( ) {
var d = new Datastore ( { filename : hookTestFilename , autoload : true
, afterSerialization : as
, beforeDeserialization : bd
} )
;
d . insert ( { hello : "world" } , function ( ) {
d . update ( { hello : "world" } , { $set : { hello : "earth" } } , { } , function ( ) {
d . ensureIndex ( { fieldName : 'idefix' } , function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
, doc0 = bd ( data [ 0 ] )
, doc1 = bd ( data [ 1 ] )
, idx = bd ( data [ 2 ] )
, _id
;
data . length . should . equal ( 4 ) ;
doc0 = model . deserialize ( doc0 ) ;
Object . keys ( doc0 ) . length . should . equal ( 2 ) ;
doc0 . hello . should . equal ( 'world' ) ;
doc1 = model . deserialize ( doc1 ) ;
Object . keys ( doc1 ) . length . should . equal ( 2 ) ;
doc1 . hello . should . equal ( 'earth' ) ;
doc0 . _id . should . equal ( doc1 . _id ) ;
_id = doc0 . _id ;
idx = model . deserialize ( idx ) ;
assert . deepEqual ( idx , { '$$indexCreated' : { fieldName : 'idefix' } } ) ;
d . persistence . persistCachedDatabase ( function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
, doc0 = bd ( data [ 0 ] )
, idx = bd ( data [ 1 ] )
;
data . length . should . equal ( 3 ) ;
doc0 = model . deserialize ( doc0 ) ;
Object . keys ( doc0 ) . length . should . equal ( 2 ) ;
doc0 . hello . should . equal ( 'earth' ) ;
doc0 . _id . should . equal ( _id ) ;
idx = model . deserialize ( idx ) ;
assert . deepEqual ( idx , { '$$indexCreated' : { fieldName : 'idefix' , unique : false , sparse : false } } ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( "Deserialization hook is correctly used when loading data" , function ( done ) {
var hookTestFilename = 'workspace/hookTest.db'
storage . ensureFileDoesntExist ( hookTestFilename , function ( ) {
var d = new Datastore ( { filename : hookTestFilename , autoload : true
, afterSerialization : as
, beforeDeserialization : bd
} )
;
d . insert ( { hello : "world" } , function ( err , doc ) {
var _id = doc . _id ;
d . insert ( { yo : "ya" } , function ( ) {
d . update ( { hello : "world" } , { $set : { hello : "earth" } } , { } , function ( ) {
d . remove ( { yo : "ya" } , { } , function ( ) {
d . ensureIndex ( { fieldName : 'idefix' } , function ( ) {
var _data = fs . readFileSync ( hookTestFilename , 'utf8' )
, data = _data . split ( '\n' )
;
data . length . should . equal ( 6 ) ;
// Everything is deserialized correctly, including deletes and indexes
var d = new Datastore ( { filename : hookTestFilename
, afterSerialization : as
, beforeDeserialization : bd
} )
;
d . loadDatabase ( function ( ) {
d . find ( { } , function ( err , docs ) {
docs . length . should . equal ( 1 ) ;
docs [ 0 ] . hello . should . equal ( "earth" ) ;
docs [ 0 ] . _id . should . equal ( _id ) ;
Object . keys ( d . indexes ) . length . should . equal ( 2 ) ;
Object . keys ( d . indexes ) . indexOf ( "idefix" ) . should . not . equal ( - 1 ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ; // ==== End of 'Serialization hooks' ==== //
describe ( 'Prevent dataloss when persisting data' , function ( ) {
it ( 'Creating a datastore with in memory as true and a bad filename wont cause an error' , function ( ) {
new Datastore ( { filename : 'workspace/bad.db~' , inMemoryOnly : true } ) ;
} )
it ( 'Creating a persistent datastore with a bad filename will cause an error' , function ( ) {
( function ( ) { new Datastore ( { filename : 'workspace/bad.db~' } ) ; } ) . should . throw ( ) ;
} )
it ( 'If no file exists, ensureDatafileIntegrity creates an empty datafile' , function ( done ) {
var p = new Persistence ( { db : { inMemoryOnly : false , filename : 'workspace/it.db' } } ) ;
if ( fs . existsSync ( 'workspace/it.db' ) ) { fs . unlinkSync ( 'workspace/it.db' ) ; }
if ( fs . existsSync ( 'workspace/it.db~' ) ) { fs . unlinkSync ( 'workspace/it.db~' ) ; }
fs . existsSync ( 'workspace/it.db' ) . should . equal ( false ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
storage . ensureDatafileIntegrity ( p . filename , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
fs . readFileSync ( 'workspace/it.db' , 'utf8' ) . should . equal ( '' ) ;
done ( ) ;
} ) ;
} ) ;
it ( 'If only datafile exists, ensureDatafileIntegrity will use it' , function ( done ) {
var p = new Persistence ( { db : { inMemoryOnly : false , filename : 'workspace/it.db' } } ) ;
if ( fs . existsSync ( 'workspace/it.db' ) ) { fs . unlinkSync ( 'workspace/it.db' ) ; }
if ( fs . existsSync ( 'workspace/it.db~' ) ) { fs . unlinkSync ( 'workspace/it.db~' ) ; }
fs . writeFileSync ( 'workspace/it.db' , 'something' , 'utf8' ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
storage . ensureDatafileIntegrity ( p . filename , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
fs . readFileSync ( 'workspace/it.db' , 'utf8' ) . should . equal ( 'something' ) ;
done ( ) ;
} ) ;
} ) ;
it ( 'If temp datafile exists and datafile doesnt, ensureDatafileIntegrity will use it (cannot happen except upon first use)' , function ( done ) {
var p = new Persistence ( { db : { inMemoryOnly : false , filename : 'workspace/it.db' } } ) ;
if ( fs . existsSync ( 'workspace/it.db' ) ) { fs . unlinkSync ( 'workspace/it.db' ) ; }
if ( fs . existsSync ( 'workspace/it.db~' ) ) { fs . unlinkSync ( 'workspace/it.db~~' ) ; }
fs . writeFileSync ( 'workspace/it.db~' , 'something' , 'utf8' ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( false ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( true ) ;
storage . ensureDatafileIntegrity ( p . filename , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
fs . readFileSync ( 'workspace/it.db' , 'utf8' ) . should . equal ( 'something' ) ;
done ( ) ;
} ) ;
} ) ;
// Technically it could also mean the write was successful but the rename wasn't, but there is in any case no guarantee that the data in the temp file is whole so we have to discard the whole file
it ( 'If both temp and current datafiles exist, ensureDatafileIntegrity will use the datafile, as it means that the write of the temp file failed' , function ( done ) {
var theDb = new Datastore ( { filename : 'workspace/it.db' } ) ;
if ( fs . existsSync ( 'workspace/it.db' ) ) { fs . unlinkSync ( 'workspace/it.db' ) ; }
if ( fs . existsSync ( 'workspace/it.db~' ) ) { fs . unlinkSync ( 'workspace/it.db~' ) ; }
fs . writeFileSync ( 'workspace/it.db' , '{"_id":"0","hello":"world"}' , 'utf8' ) ;
fs . writeFileSync ( 'workspace/it.db~' , '{"_id":"0","hello":"other"}' , 'utf8' ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( true ) ;
storage . ensureDatafileIntegrity ( theDb . persistence . filename , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( true ) ;
fs . readFileSync ( 'workspace/it.db' , 'utf8' ) . should . equal ( '{"_id":"0","hello":"world"}' ) ;
theDb . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
theDb . find ( { } , function ( err , docs ) {
assert . isNull ( err ) ;
docs . length . should . equal ( 1 ) ;
docs [ 0 ] . hello . should . equal ( "world" ) ;
fs . existsSync ( 'workspace/it.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/it.db~' ) . should . equal ( false ) ;
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'persistCachedDatabase should update the contents of the datafile and leave a clean state' , function ( done ) {
d . insert ( { hello : 'world' } , function ( ) {
d . find ( { } , function ( err , docs ) {
docs . length . should . equal ( 1 ) ;
if ( fs . existsSync ( testDb ) ) { fs . unlinkSync ( testDb ) ; }
if ( fs . existsSync ( testDb + '~' ) ) { fs . unlinkSync ( testDb + '~' ) ; }
fs . existsSync ( testDb ) . should . equal ( false ) ;
fs . writeFileSync ( testDb + '~' , 'something' , 'utf8' ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( true ) ;
d . persistence . persistCachedDatabase ( function ( err ) {
var contents = fs . readFileSync ( testDb , 'utf8' ) ;
assert . isNull ( err ) ;
fs . existsSync ( testDb ) . should . equal ( true ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( false ) ;
if ( ! contents . match ( /^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/ ) ) {
throw "Datafile contents not as expected" ;
}
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'After a persistCachedDatabase, there should be no temp or old filename' , function ( done ) {
d . insert ( { hello : 'world' } , function ( ) {
d . find ( { } , function ( err , docs ) {
docs . length . should . equal ( 1 ) ;
if ( fs . existsSync ( testDb ) ) { fs . unlinkSync ( testDb ) ; }
if ( fs . existsSync ( testDb + '~' ) ) { fs . unlinkSync ( testDb + '~' ) ; }
fs . existsSync ( testDb ) . should . equal ( false ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( false ) ;
fs . writeFileSync ( testDb + '~' , 'bloup' , 'utf8' ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( true ) ;
d . persistence . persistCachedDatabase ( function ( err ) {
var contents = fs . readFileSync ( testDb , 'utf8' ) ;
assert . isNull ( err ) ;
fs . existsSync ( testDb ) . should . equal ( true ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( false ) ;
if ( ! contents . match ( /^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/ ) ) {
throw "Datafile contents not as expected" ;
}
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'persistCachedDatabase should update the contents of the datafile and leave a clean state even if there is a temp datafile' , function ( done ) {
d . insert ( { hello : 'world' } , function ( ) {
d . find ( { } , function ( err , docs ) {
docs . length . should . equal ( 1 ) ;
if ( fs . existsSync ( testDb ) ) { fs . unlinkSync ( testDb ) ; }
fs . writeFileSync ( testDb + '~' , 'blabla' , 'utf8' ) ;
fs . existsSync ( testDb ) . should . equal ( false ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( true ) ;
d . persistence . persistCachedDatabase ( function ( err ) {
var contents = fs . readFileSync ( testDb , 'utf8' ) ;
assert . isNull ( err ) ;
fs . existsSync ( testDb ) . should . equal ( true ) ;
fs . existsSync ( testDb + '~' ) . should . equal ( false ) ;
if ( ! contents . match ( /^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/ ) ) {
throw "Datafile contents not as expected" ;
}
done ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
it ( 'persistCachedDatabase should update the contents of the datafile and leave a clean state even if there is a temp datafile' , function ( done ) {
var dbFile = 'workspace/test2.db' , theDb ;
if ( fs . existsSync ( dbFile ) ) { fs . unlinkSync ( dbFile ) ; }
if ( fs . existsSync ( dbFile + '~' ) ) { fs . unlinkSync ( dbFile + '~' ) ; }
theDb = new Datastore ( { filename : dbFile } ) ;
theDb . loadDatabase ( function ( err ) {
var contents = fs . readFileSync ( dbFile , 'utf8' ) ;
assert . isNull ( err ) ;
fs . existsSync ( dbFile ) . should . equal ( true ) ;
fs . existsSync ( dbFile + '~' ) . should . equal ( false ) ;
if ( contents != "" ) {
throw "Datafile contents not as expected" ;
}
done ( ) ;
} ) ;
} ) ;
it ( 'Persistence works as expected when everything goes fine' , function ( done ) {
var dbFile = 'workspace/test2.db' , theDb , theDb2 , doc1 , doc2 ;
async . waterfall ( [
async . apply ( storage . ensureFileDoesntExist , dbFile )
, async . apply ( storage . ensureFileDoesntExist , dbFile + '~' )
, function ( cb ) {
theDb = new Datastore ( { filename : dbFile } ) ;
theDb . loadDatabase ( cb ) ;
}
, function ( cb ) {
theDb . find ( { } , function ( err , docs ) {
assert . isNull ( err ) ;
docs . length . should . equal ( 0 ) ;
return cb ( ) ;
} ) ;
}
, function ( cb ) {
theDb . insert ( { a : 'hello' } , function ( err , _doc1 ) {
assert . isNull ( err ) ;
doc1 = _doc1 ;
theDb . insert ( { a : 'world' } , function ( err , _doc2 ) {
assert . isNull ( err ) ;
doc2 = _doc2 ;
return cb ( ) ;
} ) ;
} ) ;
}
, function ( cb ) {
theDb . find ( { } , function ( err , docs ) {
assert . isNull ( err ) ;
docs . length . should . equal ( 2 ) ;
_ . find ( docs , function ( item ) { return item . _id === doc1 . _id } ) . a . should . equal ( 'hello' ) ;
_ . find ( docs , function ( item ) { return item . _id === doc2 . _id } ) . a . should . equal ( 'world' ) ;
return cb ( ) ;
} ) ;
}
, function ( cb ) {
theDb . loadDatabase ( cb ) ;
}
, function ( cb ) { // No change
theDb . find ( { } , function ( err , docs ) {
assert . isNull ( err ) ;
docs . length . should . equal ( 2 ) ;
_ . find ( docs , function ( item ) { return item . _id === doc1 . _id } ) . a . should . equal ( 'hello' ) ;
_ . find ( docs , function ( item ) { return item . _id === doc2 . _id } ) . a . should . equal ( 'world' ) ;
return cb ( ) ;
} ) ;
}
, function ( cb ) {
fs . existsSync ( dbFile ) . should . equal ( true ) ;
fs . existsSync ( dbFile + '~' ) . should . equal ( false ) ;
return cb ( ) ;
}
, function ( cb ) {
theDb2 = new Datastore ( { filename : dbFile } ) ;
theDb2 . loadDatabase ( cb ) ;
}
, function ( cb ) { // No change in second db
theDb2 . find ( { } , function ( err , docs ) {
assert . isNull ( err ) ;
docs . length . should . equal ( 2 ) ;
_ . find ( docs , function ( item ) { return item . _id === doc1 . _id } ) . a . should . equal ( 'hello' ) ;
_ . find ( docs , function ( item ) { return item . _id === doc2 . _id } ) . a . should . equal ( 'world' ) ;
return cb ( ) ;
} ) ;
}
, function ( cb ) {
fs . existsSync ( dbFile ) . should . equal ( true ) ;
fs . existsSync ( dbFile + '~' ) . should . equal ( false ) ;
return cb ( ) ;
}
] , done ) ;
} ) ;
// This test needs to be rewritten. The goal is to check what happens when the system crashes during a writeFile, so this test
// must rewrite a custom and buggy writeFile that will be used by the child process and crash in the midst of writing the file
it ( 'If system crashes during a loadDatabase, the former version is not lost' , function ( done ) {
var cp , N = 150000 , toWrite = "" , i ;
// Ensuring the state is clean
if ( fs . existsSync ( 'workspace/lac.db' ) ) { fs . unlinkSync ( 'workspace/lac.db' ) ; }
if ( fs . existsSync ( 'workspace/lac.db~' ) ) { fs . unlinkSync ( 'workspace/lac.db~' ) ; }
// Creating a db file with 150k records (a bit long to load)
for ( i = 0 ; i < N ; i += 1 ) {
toWrite += model . serialize ( { _id : customUtils . uid ( 16 ) , hello : 'world' } ) + '\n' ;
}
fs . writeFileSync ( 'workspace/lac.db' , toWrite , 'utf8' ) ;
// Loading it in a separate process that we will crash before finishing the loadDatabase
cp = child _process . fork ( 'test_lac/loadAndCrash.test' )
// Kill the child process when we're at step 3 of persistCachedDatabase (during write to datafile)
setTimeout ( function ( ) {
cp . kill ( 'SIGINT' ) ;
// If the timing is correct, only the temp datafile contains data
// The datafile was in the middle of being written and is empty
// Let the process crash be finished then load database without a crash, and test we didn't lose data
setTimeout ( function ( ) {
var db = new Datastore ( { filename : 'workspace/lac.db' } ) ;
db . loadDatabase ( function ( err ) {
assert . isNull ( err ) ;
db . count ( { } , function ( err , n ) {
// Data has not been lost
assert . isNull ( err ) ;
n . should . equal ( 150000 ) ;
// State is clean, the temp datafile has been erased and the datafile contains all the data
fs . existsSync ( 'workspace/lac.db' ) . should . equal ( true ) ;
fs . existsSync ( 'workspace/lac.db~' ) . should . equal ( false ) ;
done ( ) ;
} ) ;
} ) ;
} , 100 ) ;
} , 2000 ) ;
} ) ;
} ) ; // ==== End of 'Prevent dataloss when persisting data' ====
describe ( 'ensureFileDoesntExist' , function ( ) {
it ( 'Doesnt do anything if file already doesnt exist' , function ( done ) {
storage . ensureFileDoesntExist ( 'workspace/nonexisting' , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/nonexisting' ) . should . equal ( false ) ;
done ( ) ;
} ) ;
} ) ;
it ( 'Deletes file if it exists' , function ( done ) {
fs . writeFileSync ( 'workspace/existing' , 'hello world' , 'utf8' ) ;
fs . existsSync ( 'workspace/existing' ) . should . equal ( true ) ;
storage . ensureFileDoesntExist ( 'workspace/existing' , function ( err ) {
assert . isNull ( err ) ;
fs . existsSync ( 'workspace/existing' ) . should . equal ( false ) ;
done ( ) ;
} ) ;
} ) ;
} ) ; // ==== End of 'ensureFileDoesntExist' ====
} ) ;