foundationdb/bindings/nodejs/tests/tester.js

753 lines
18 KiB
JavaScript
Executable File

/*
* tester.js
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
*
* 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.
*/
#!/usr/bin/env node
"use strict";
//cmd line: node tester.js <test_prefix> <optional_cluster_file>
var startTestPrefix = process.argv[2];
if(process.argv.length === 5)
var clusterFile = process.argv[4];
else
var clusterFile = '';
var assert = require('assert');
var fdb = require('../lib/fdb.js').apiVersion(parseInt(process.argv[3]));
var fdbUtil = require('../lib/fdbUtil.js');
var testerUtil = require('./util.js');
var DirectoryExtension = require('./directory_extension.js');
//fdb.options.setTraceEnable()
var db = fdb.open(clusterFile);
function pushError(inst, err) {
if(err) {
if(!err.code) {
console.error('ERROR during operation \'' + inst.op + '\':');
console.error(err.stack);
context.cb(err);
}
inst.push(fdb.tuple.pack([fdb.buffer('ERROR'), fdb.buffer(err.code.toString())]));
}
return err;
}
var rangeChoice = 0;
function pushRange(itr, inst, prefixFilter, cb) {
return fdb.future.create(function(futureCb) {
var outArray = [];
function pushKV(kv) {
if(typeof prefixFilter === 'undefined' || prefixFilter === null || fdbUtil.buffersEqual(kv.key.slice(0, prefixFilter.length), prefixFilter)) {
outArray.push(kv.key);
outArray.push(kv.value);
}
}
function finish(err) {
if(!pushError(inst, err))
inst.push(fdb.tuple.pack(outArray));
futureCb();
}
//Test different methods for getting a range
if(inst.isDatabase) {
for(var i = 0; i < itr.length; ++i)
pushKV(itr[i]);
finish();
}
else if(rangeChoice % 4 === 0) {
itr.forEachBatch(function(res, itrCb) {
for(var i = 0 ; i < res.length; ++i)
pushKV(res[i]);
if(rangeChoice % 8 === 0)
setTimeout(itrCb, 0);
else
itrCb();
}, function(err, res) {
finish(err);
});
}
else if(rangeChoice % 4 === 1) {
itr.forEach(function(res, itrCb) {
pushKV(res);
if(rangeChoice % 8 === 1)
setTimeout(itrCb, 0);
else
itrCb();
}, function(err, res) {
finish(err);
});
}
else if(rangeChoice % 4 === 2) {
itr.toArray(function(err, arr) {
if(!err) {
for(var i = 0; i < arr.length; ++i)
pushKV(arr[i]);
}
finish(err);
});
}
else {
fdbUtil.whileLoop(function(loopCb) {
itr.next(function(err, res) {
if(err)
loopCb(err);
else if(!res)
loopCb(undefined, null);
else {
pushKV(res);
loopCb();
}
});
}, finish);
}
rangeChoice++;
})(cb);
}
var waitEmpty = fdb.transactional(function(tr, prefix, cb) {
var itr = tr.getRangeStartsWith(prefix, { limit: 1 });
itr.toArray(function(err, res) {
if(err)
cb(err, null);
else if(res.length > 0)
cb(new fdb.FDBError('', 1020), null);
else
cb(null, null);
});
});
var timeoutFuture = function(time) {
return fdb.future.create(function(futureCb) {
setTimeout(futureCb, time);
});
};
var checkWatches = function(db, watches, ready, error, expected, cb) {
var i = 0;
return fdbUtil.whileLoop(function(loopCb) {
if(i == watches.length) return loopCb(undefined, true); // terminate loop
if(!ready[i] && expected) {
return watches[i]
.then(function() {
loopCb(); // Recheck this watch when it finishes
})
.catch(function() {
loopCb(); // Check the error
});
}
assert.strictEqual(!ready[i] || expected, true, 'watch shouldnt be ready: ' + i);
if(typeof error[i] !== 'undefined') {
var tr = db.createTransaction();
return tr.onError(error[i])
.then(function() {
return false;
})(loopCb);
}
i++;
loopCb();
})(cb);
}
var testWatches = function(db, cb) {
return fdbUtil.whileLoop(function(loopCb) {
var ready = [ false, false, false, false ];
var error = [ undefined, undefined, undefined, undefined ];
var watches = [];
db.doTransaction(function(tr, innerCb) {
tr.set('w0', '0')
tr.set('w3', '3');
innerCb();
})
.then(function() {
return db.doTransaction(function(tr, innerCb) {
watches[0] = tr.watch('w0');
innerCb();
});
})
.then(function() {
return db.clearAndWatch('w1');
})
.then(function(w) {
watches[1] = w.watch;
return db.setAndWatch('w2', '2');
})
.then(function(w) {
watches[2] = w.watch;
return db.getAndWatch('w3');
})
.then(function(w) {
assert.strictEqual(w.value.toString(), '3', 'get and watch');
watches[3] = w.watch;
for(var i = 0; i < watches.length; ++i) {
(function(i) {
watches[i](function(err) {
if(!err) ready[i] = true;
else error[i] = err;
});
})(i);
}
return timeoutFuture(1000);
})
.then(function() {
return checkWatches(db, watches, ready, error, false);
})
.then(function(result) {
if(!result) return; // go around the loop again
return db.doTransaction(function(tr, innerCb) {
tr.set('w0', '0');
innerCb();
})
.then(function() {
return db.clear('w1');
})
.then(function() {
return timeoutFuture(5000);
})
.then(function() {
return checkWatches(db, watches, ready, error, false);
})
.then(function(result) {
if(!result) return; // go around the loop again
return db.set('w0', 'a')
.then(function() {
return db.set('w1', 'b');
})
.then(function() {
return db.clear('w2');
})
.then(function() {
return db.xor('w3', fdb.buffer.fromByteLiteral('\xff\xff'));
})
.then(function() {
return timeoutFuture(2000);
})
.then(function() {
return checkWatches(db, watches, ready, error, true);
})
.then(function(result) {
if(result) return null; //terminate loop
return;
});
});
})(loopCb);
})(cb);
};
var testLocality = function(db, cb) {
return db.doTransaction(function(tr, innerCb) {
tr.options.setTimeout(60*1000);
tr.options.setReadSystemKeys();
fdb.locality.getBoundaryKeys(tr, '', fdb.buffer.fromByteLiteral('\xff\xff'), function(err, itr) {
if(err) return innerCb(err);
var index = 0;
var start;
var end;
itr.forEach(function(boundaryKey, loopCb) {
if(err) return loopCb(err);
start = end;
end = boundaryKey;
if(index++ == 0)
return loopCb();
tr.getKey(fdb.KeySelector.lastLessThan(end), function(err, end) {
if(err) return loopCb(err);
fdb.locality.getAddressesForKey(tr, start, function(err, startAddresses) {
if(err) return loopCb(err);
fdb.locality.getAddressesForKey(tr, end, function(err, endAddresses) {
if(err) return loopCb(err);
for(var j = 0; j < startAddresses.length; ++j) {
var found = false;
for(var k = 0; k < endAddresses.length; ++k) {
if(startAddresses[j].toString() === endAddresses[k].toString()) {
found = true;
break;
}
}
if(!found) {
return loopCb(new Error('Locality not internally consistent'));
}
}
loopCb();
});
});
});
}, innerCb);
});
}, cb);
};
var numOperations = 0;
function processOperation(context, inst, cb) {
//if(inst.op !== 'SWAP' && inst.op !== 'PUSH')
//console.log(context.prefix + ':', context.instructionIndex + '.', inst.op);
var promiseCb = function(err) {
pushError(inst, err);
cb();
};
if(inst.op === 'PUSH') {
inst.push(inst.tokens[1]);
cb();
}
else if(inst.op === 'POP') {
inst.pop()(promiseCb);
}
else if(inst.op === 'DUP') {
context.stack.pushEntry(context.stack.get(context.stack.length()-1));
cb();
}
else if(inst.op === 'EMPTY_STACK') {
context.stack = new testerUtil.Stack();
cb();
}
else if(inst.op === 'SWAP') {
inst.pop()
.then(function(index) {
assert.strictEqual(context.stack.length() > index, true, 'Cannot swap; stack too small');
index = context.stack.length() - index - 1;
if(context.stack.length() > index + 1) {
var tmp = context.stack.get(index);
context.stack.set(index, context.stack.popEntry());
context.stack.pushEntry(tmp);
}
})(promiseCb);
}
else if(inst.op === 'WAIT_FUTURE') {
inst.pop({withMetadata: true})
.then(function(stackEntry) {
context.stack.pushEntry(stackEntry);
})(promiseCb);
}
else if(inst.op === 'WAIT_EMPTY') {
inst.pop()
.then(function(waitKey) {
return waitEmpty(db, waitKey)
.then(function() {
inst.push('WAITED_FOR_EMPTY');
});
})(promiseCb);
}
else if(inst.op === 'START_THREAD') {
inst.pop()
.then(function(prefix) {
processTest(prefix, function(err, res) {
if(err) {
console.error('ERROR in Thread', prefix + ':');
console.error(err.stack);
process.exit(1);
}
});
})(promiseCb);
}
else if(inst.op === 'NEW_TRANSACTION') {
context.newTransaction();
cb();
}
else if(inst.op === 'USE_TRANSACTION') {
inst.pop()
.then(function(name) {
context.switchTransaction(name);
})(promiseCb);
}
else if(inst.op === 'SET') {
inst.pop({count: 2})
.then(function(params) {
var res = inst.tr.set(params[0], params[1]);
if(inst.isDatabase)
inst.push(res, true);
})(promiseCb);
}
else if(inst.op === 'CLEAR') {
inst.pop()
.then(function(key) {
var res = inst.tr.clear(key);
if(inst.isDatabase)
inst.push(res, true);
})(promiseCb);
}
else if(inst.op === 'CLEAR_RANGE') {
inst.pop({count: 2})
.then(function(params) {
var res = inst.tr.clearRange(params[0], params[1]);
if(inst.isDatabase)
inst.push(res, true);
})(promiseCb);
}
else if(inst.op === 'CLEAR_RANGE_STARTS_WITH') {
inst.pop()
.then(function(prefix) {
var res = inst.tr.clearRangeStartsWith(prefix);
if(inst.isDatabase)
inst.push(res, true);
})(promiseCb);
}
else if(inst.op === 'ATOMIC_OP') {
inst.pop({count: 3})
.then(function(params) {
var res = inst.tr[testerUtil.toJavaScriptName(params[0])](params[1], params[2]);
if(inst.isDatabase)
inst.push(res, true);
})(promiseCb);
}
else if(inst.op === 'COMMIT') {
inst.push(inst.tr.commit(), true);
cb();
}
else if(inst.op === 'RESET') {
inst.tr.reset();
cb();
}
else if(inst.op === 'CANCEL') {
inst.tr.cancel();
cb();
}
else if(inst.op === 'GET') {
inst.pop()
.then(function(key) {
inst.push(inst.tr.get(key), true);
})(promiseCb);
}
else if(inst.op === 'GET_RANGE') {
inst.pop({count: 5})
.then(function(params) {
var itr = inst.tr.getRange(params[0], params[1], { limit: params[2], reverse: params[3], streamingMode: params[4] });
if(inst.isDatabase) {
return itr.then(function(arr) {
return pushRange(arr, inst);
});
}
else {
return pushRange(itr, inst);
}
})(promiseCb);
}
else if(inst.op === 'GET_RANGE_SELECTOR') {
inst.pop({count: 10})
.then(function(params) {
var start = new fdb.KeySelector(params[0], params[1], params[2]);
var end = new fdb.KeySelector(params[3], params[4], params[5]);
var itr = inst.tr.getRange(start, end, { limit: params[6], reverse: params[7], streamingMode: params[8] });
if(inst.isDatabase) {
return itr.then(function(arr) {
return pushRange(arr, inst, params[9]);
});
}
else {
return pushRange(itr, inst, params[9]);
}
})(promiseCb);
}
else if(inst.op === 'GET_RANGE_STARTS_WITH') {
inst.pop({count: 4})
.then(function(params) {
var itr = inst.tr.getRangeStartsWith(params[0], { limit: params[1], reverse: params[2], streamingMode: params[3] });
if(inst.isDatabase) {
return itr.then(function(arr) {
return pushRange(arr, inst);
});
}
else {
return pushRange(itr, inst);
}
})(promiseCb);
}
else if(inst.op === 'GET_KEY') {
inst.pop({count: 4})
.then(function(params) {
var result = inst.tr.getKey(new fdb.KeySelector(params[0], params[1], params[2]))
.then(function(key) {
if(fdbUtil.buffersEqual(key.slice(0, params[3].length), params[3])) {
return key;
}
else if(fdb.buffer.toByteLiteral(key) < fdb.buffer.toByteLiteral(params[3])) {
return params[3];
}
else {
return fdbUtil.strinc(params[3]);
}
});
inst.push(result, true);
})(promiseCb);
}
else if(inst.op === 'READ_CONFLICT_RANGE') {
inst.pop({count: 2})
.then(function(params) {
inst.tr.addReadConflictRange(params[0], params[1]);
inst.push(fdb.buffer('SET_CONFLICT_RANGE'));
})(promiseCb);
}
else if(inst.op === 'WRITE_CONFLICT_RANGE') {
inst.pop({count: 2})
.then(function(params) {
inst.tr.addWriteConflictRange(params[0], params[1]);
inst.push(fdb.buffer('SET_CONFLICT_RANGE'));
})(promiseCb);
}
else if(inst.op === 'READ_CONFLICT_KEY') {
inst.pop()
.then(function(key) {
inst.tr.addReadConflictKey(key);
inst.push(fdb.buffer('SET_CONFLICT_KEY'));
})(promiseCb);
}
else if(inst.op === 'WRITE_CONFLICT_KEY') {
inst.pop()
.then(function(key) {
inst.tr.addWriteConflictKey(key);
inst.push(fdb.buffer('SET_CONFLICT_KEY'));
})(promiseCb);
}
else if(inst.op === 'DISABLE_WRITE_CONFLICT') {
inst.tr.options.setNextWriteNoWriteConflictRange();
cb();
}
else if(inst.op === 'GET_READ_VERSION') {
inst.tr.getReadVersion(function(err, res) {
if(!pushError(inst, err)) {
context.lastVersion = res;
inst.push(fdb.buffer('GOT_READ_VERSION'));
}
cb();
});
}
else if(inst.op === 'GET_COMMITTED_VERSION') {
try {
context.lastVersion = inst.tr.getCommittedVersion();
inst.push(fdb.buffer('GOT_COMMITTED_VERSION'));
cb();
}
catch(err) {
pushError(inst, err);
cb();
}
}
else if(inst.op === 'GET_VERSIONSTAMP') {
try {
inst.push(inst.tr.getVersionstamp(), true)
cb();
}
catch(err) {
pushError(inst, err);
cb();
}
}
else if(inst.op === 'SET_READ_VERSION') {
assert.notStrictEqual(typeof context.lastVersion, 'undefined', 'Cannot set read version; version has never been read');
inst.tr.setReadVersion(context.lastVersion);
cb();
}
else if(inst.op === 'ON_ERROR') {
inst.pop()
.then(function(errorCode) {
var testErr = new fdb.FDBError('', errorCode);
inst.push(inst.tr.onError(testErr), true);
})(promiseCb);
}
else if(inst.op === 'TUPLE_PACK') {
inst.pop()
.then(function(numParams) {
return inst.pop({count: numParams})
.then(function(params) {
inst.push(fdb.tuple.pack(params));
});
})(promiseCb);
}
else if(inst.op === 'TUPLE_UNPACK') {
inst.pop()
.then(function(packedTuple) {
var arr = fdb.tuple.unpack(packedTuple);
for(var i = 0; i < arr.length; ++i)
inst.push(fdb.tuple.pack([arr[i]]));
})(promiseCb);
}
else if(inst.op === 'TUPLE_RANGE') {
inst.pop()
.then(function(numParams) {
return inst.pop({count: numParams})
.then(function(params) {
var range = fdb.tuple.range(params);
inst.push(range.begin);
inst.push(range.end);
});
})(promiseCb);
}
else if(inst.op === 'TUPLE_SORT') {
inst.pop()
.then(function(numParams) {
return inst.pop({count: numParams})
.then(function(params) {
var tuples = [];
for(var i = 0; i < params.length; ++i)
tuples.push(fdb.tuple.unpack(params[i]));
tuples.sort(fdb.tuple.compare);
for(var i = 0; i < tuples.length; ++i)
inst.push(fdb.tuple.pack(tuples[i]));
});
})(promiseCb);
}
else if(inst.op === 'SUB') {
inst.pop({count: 2})
.then(function(params) {
inst.push(params[0] - params[1]);
})(promiseCb);
}
else if(inst.op === 'ENCODE_FLOAT') {
inst.pop()
.then(function(fBytes) {
inst.push(fdb.tuple.Float.fromBytes(fBytes));
})(promiseCb);
}
else if(inst.op === 'ENCODE_DOUBLE') {
inst.pop()
.then(function(dBytes) {
inst.push(fdb.tuple.Double.fromBytes(dBytes));
})(promiseCb);
}
else if(inst.op === 'DECODE_FLOAT') {
inst.pop()
.then(function(fVal) {
inst.push(fVal.toBytes());
})(promiseCb);
}
else if(inst.op === 'DECODE_DOUBLE') {
inst.pop()
.then(function(dVal) {
inst.push(dVal.toBytes());
})(promiseCb);
}
else if(inst.op === 'CONCAT') {
inst.pop({count: 2})
.then(function(params) {
if(Buffer.isBuffer(params[0])) {
inst.push(Buffer.concat([params[0], params[1]]))
}
else {
inst.push(params[0] + params[1]);
}
})(promiseCb);
}
else if(inst.op === 'LOG_STACK') {
inst.pop()
.then(function(prefix) {
return fdbUtil.whileLoop(function(loopCb) {
inst.pop({count: 100, withMetadata: true})
.then(function(items) {
if(items.length == 0) {
return null
}
return db.doTransaction(function(tr, innerCb) {
for(var index = 0; index < items.length; ++index) {
var entry = items[items.length - index - 1];
var packedSubKey = fdb.tuple.pack([context.stack.length() + index, entry.instructionIndex]);
var packedValue = fdb.tuple.pack([entry.item]);
if(packedValue.length > 40000)
packedValue = packedValue.slice(0, 40000);
tr.set(Buffer.concat([prefix, packedSubKey], prefix.length + packedSubKey.length), packedValue);
}
innerCb();
});
})(loopCb);
});
})(promiseCb);
}
else if(inst.op === 'UNIT_TESTS') {
db.options.setLocationCacheSize(100001);
db.doTransaction(function(tr, innerCb) {
tr.options.setPrioritySystemImmediate();
tr.options.setPriorityBatch();
tr.options.setCausalReadRisky();
tr.options.setCausalWriteRisky();
tr.options.setReadYourWritesDisable();
tr.options.setReadAheadDisable();
tr.options.setReadSystemKeys();
tr.options.setAccessSystemKeys();
tr.options.setDurabilityDevNullIsWebScale();
tr.options.setTimeout(60*1000);
tr.options.setRetryLimit(50);
tr.options.setMaxRetryDelay(100);
tr.options.setUsedDuringCommitProtectionDisable();
tr.options.setTransactionLoggingEnable('my_transaction');
tr.get(fdb.buffer.fromByteLiteral('\xff'), innerCb);
})
.then(function() {
return testWatches(db);
})
.then(function() {
return testLocality(db);
})
.then(cb)
.catch(function(err) {
cb('Unit tests failed: ' + err + "\n" + err.stack);
});
}
else if(testerUtil.startsWith(inst.op, 'DIRECTORY_')) {
context.directoryExtension.processInstruction(inst, cb);
}
else {
cb('Unrecognized operation');
}
}
function processTest(prefix, cb) {
var context = new testerUtil.Context(db, prefix, processOperation, new DirectoryExtension());
context.run(cb);
}
processTest(startTestPrefix, function(err, res) {
if(err)
process.exit(1);
});