exam
Version:
Clustered JavaScript test runner with builtin assertions.
466 lines (428 loc) • 11.6 kB
JavaScript
/**
* Keep track of mocked objects so they can all be unmocked.
*/
var mockedObjects = []
/**
* Allow `mock-fs` to be loaded lazily and referenced.
*/
var mockFs
/**
* Decorate an object with mocked properties.
*/
var mock = module.exports = function mock (object, mockObject) {
var mocked = object._EXAM_MOCKED_ORIGINALS
if (!mocked) {
mocked = [{}, {}]
Object.defineProperty(object, '_EXAM_MOCKED_ORIGINALS', {
enumerable: false,
value: mocked
})
}
var pro = mocked[0]
var own = mocked[1]
for (var key in mockObject) {
var index = object.hasOwnProperty(key) ? 1 : 0
// Only remember one original, whether prototype or own.
if (typeof pro[key] === 'undefined' && typeof own[key] === 'undefined') {
mocked[index][key] = object[key]
}
object[key] = mockObject[key]
}
mockedObjects.push(object)
return object
}
/**
* Restore mocked properties to their mocked values.
*/
var unmock = mock.unmock = function unmock (object, keys) {
// If called with no arguments, unmock all objects that have been mocked.
if (!arguments.length) {
mockedObjects.forEach(function (object) {
unmock(object)
})
mockedObjects.length = 0
unmock.fs()
unmock.time()
return
}
// If it's a single key, make it an array.
if (typeof keys === 'string') {
keys = [keys]
}
// If the object has original values for mocked properties, restore them.
var mocked = object._EXAM_MOCKED_ORIGINALS
if (mocked) {
for (var index = 0; index < 2; index++) {
var originals = keys || mocked[index]
for (var key in originals) {
if (index) {
object[key] = originals[key]
} else {
delete object[key]
}
}
}
// If we weren't deleting specific keys, we no longer need the originals.
if (!keys) {
delete object._EXAM_MOCKED_ORIGINALS
}
}
return object
}
/**
* Decorate a mock function with chainable methods.
*/
function decorateFn (fn) {
fn.returns = function (value) {
fn._returns = value
return fn
}
return fn
}
/**
* Finish the execution of a mock function.
*/
function finishFn (fn) {
return fn._returns
}
/**
* Simple mock function: Ignore.
*/
mock.fn = mock.ignore = function () {
var fn = function () {
return finishFn(fn)
}
return decorateFn(fn)
}
/**
* Simple mock function: Call counter.
*/
mock.count = function () {
function fn () {
fn.value++
return finishFn(fn)
}
fn.value = 0
return decorateFn(fn)
}
/**
* Simple mock function: Argument setter.
*/
mock.set = function (index) {
function fn () {
fn.value = isNaN(index) ? arguments : arguments[index]
return finishFn(fn)
}
fn.value = []
return decorateFn(fn)
}
/**
* Simple mock function: Argument pusher.
*/
mock.args = function (index) {
function fn () {
fn.value.push(isNaN(index) ? arguments : arguments[index])
return finishFn(fn)
}
fn.value = []
return decorateFn(fn)
}
/**
* Simple mock function: String concatenator.
*/
mock.concat = function (delimiter) {
delimiter = delimiter || ''
function fn (data) {
fn.value += (fn.value ? delimiter : '') + data
return finishFn(fn)
}
fn.value = ''
return decorateFn(fn)
}
/**
* Simple mock function: Error thrower.
*/
mock.throw = function (message) {
function fn () {
var error = new Error(message)
error.arguments = arguments
fn.value.push(error)
throw error
}
fn.value = []
return decorateFn(fn)
}
// Make a reference to the real file system before mocking.
var fs = require('fs')
/**
* Load `mock-fs` and create a temporary file system.
*/
mock.fs = function (config, newFs) {
mockFs = require('mock-fs')
var fs
if (newFs) {
fs = mockFs.fs(config)
} else {
mockFs(config)
fs = require('fs')
}
return fs
}
/**
* Load an array of paths, and return an object for `mock.fs`.
*/
mock.fs.load = function (paths) {
var load = {}
paths.forEach(function (path) {
load[path] = fs.readFileSync(path)
})
return load
}
/**
* Restore the real file system.
*/
unmock.fs = function () {
if (mockFs) {
mockFs.restore()
mockFs = null
}
}
/**
* Make the os and cluster modules think there's just one CPU.
*/
mock.cpu = function (options) {
options = options || {}
if (typeof options.cpus === 'number') {
options.cpus = unmock(require('os')).cpus().slice(0, options.cpus)
}
if (typeof options.hostname === 'string') {
options.hostname = mock.fn().returns(options.hostname)
}
if (options.fork === false) {
options.fork = mock.fn()
}
var modules = ['os', 'cluster']
modules.forEach(function (lib) {
lib = require(lib)
unmock(lib)
var mocks = {}
for (var key in options) {
if (typeof lib[key] === typeof options[key]) {
mocks[key] = options[key]
}
}
mock(lib, mocks)
})
}
/**
* Unmock the cluster and os modules.
*/
unmock.cpu = function () {
var modules = ['os', 'cluster']
modules.forEach(function (lib) {
unmock(require(lib))
})
}
// Make references to timers and the real `Date` object before mocking time.
var timers = require('timers')
timers.Date = Date
/**
* Freeze time to a `Date` constructed with the given `value`.
*/
mock.time = function (value) {
var date = new timers.Date(value)
mock.time._CURRENT_TIME = date.getTime()
mock(timers.Date, {now: MockDate.now})
global.Date = MockDate
global.setTimeout = getScheduler(false)
global.setInterval = getScheduler(true)
global.clearTimeout = getUnscheduler()
global.clearInterval = getUnscheduler()
return mock.time
}
/**
* Construct a `MockDate` using a `Date` constructor value.
*/
function MockDate (value) {
// A MockDate constructs an inner date and exposes its methods.
var innerDate
// If a value is specified, use it to construct a real date.
if (arguments.length) {
innerDate = new timers.Date(value)
// If time isn't currently mocked, construct a real date for the real time.
} else if (global.Date === timers.Date) {
innerDate = new timers.Date()
// If there's no value and time is mocked, use the current mock time.
} else {
innerDate = new timers.Date(mock.time._CURRENT_TIME)
}
Object.defineProperty(this, '_INNER_DATE', {
enumerable: false,
value: innerDate
})
}
/**
* Get the current mock time or real time in milliseconds.
*/
MockDate.now = function () {
// If time has been unmocked, use the real Date object.
if (mock.time._CURRENT_TIME === undefined) {
return realNow()
// Otherwise, use the mock time value.
} else {
return mock.time._CURRENT_TIME
}
}
// `Date.parse` and `Date.UTC` are not relative to the current time.
MockDate.parse = timers.Date.parse
MockDate.UTC = timers.Date.UTC
// An instance of `MockDate` uses methods from its inner date.
var methods = ['getDate', 'getDay', 'getFullYear', 'getHours', 'getMilliseconds',
'getMinutes', 'getMonth', 'getSeconds', 'getTime', 'getTimezoneOffset',
'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours',
'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds',
'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate',
'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes',
'setUTCMonth', 'setUTCSeconds', 'setYear', 'toDateString', 'toGMTString',
'toISOString', 'toJSON', 'toLocaleDateString', 'toLocaleString',
'toLocaleTimeString', 'toString', 'toTimeString', 'toUTCString',
'valueOf']
methods.forEach(function (name) {
MockDate.prototype[name] = function () {
return this._INNER_DATE[name].apply(this._INNER_DATE, arguments)
}
})
/**
* Add time to the frozen value.
*/
mock.time.add = function (time) {
var p = /(\d+)\s*(s|seconds?)?(m|minutes?)?(h|hours?)?(d|days?)?\b/
time = '' + time
time.replace(p, function (match, n, s, m, h, d) {
mock.time._CURRENT_TIME += +n * (s ? 1e3 : m ? 6e4 : h ? 36e5 : d ? 864e5 : 1)
})
runSchedules()
}
/**
* Set `mock.time` to move forward at a faster or slower speed than
* real time, based on whether `speed` is greater or less than 1.
*/
mock.time.speed = function (speed) {
// By default, move ahead a thousand times faster than real time.
mock.time._SPEED = speed || 1e3
moveTime()
}
/**
* If mock time is moving forward, schedule a time check.
*/
function moveTime () {
if (mock.time._SPEED) {
// Remember what the real time was before updating.
mock.time._PREVIOUS_TIME = realNow()
// Set time to be incremented.
setTimeout(function () {
var now = realNow()
var elapsed = now - mock.time._PREVIOUS_TIME
if (elapsed) {
var add = elapsed * mock.time._SPEED
mock.time.add(add)
}
moveTime()
}, 0)
}
}
/**
* Return what `Date.now()` would return if it wasn't mocked.
*/
function realNow () {
// We don't replace the `Date` constructor, so this is safe from stack overflow.
return (new timers.Date()).getTime()
}
/**
* `schedules` are the result of mocked `setTimeout` and `setInterval` calls.
*/
var schedules = []
schedules.id = 0
/**
* The `getScheduler` returns mocks for `setTimeout` and `setInterval`.
*/
function getScheduler (isInterval) {
return function (fn, time) {
schedules.push({
id: ++schedules.id,
fn: fn,
time: Date.now() + time,
interval: isInterval ? time : false
})
}
}
/**
* The `getUnscheduler` returns mocks for `clearTimeout` and `clearInterval`.
*/
function getUnscheduler () {
// TODO: Create a map of IDs if the schedules array gets large.
return function (id) {
for (var i = 0, l = schedules.length; i < l; i++) {
var schedule = schedules[i]
if (schedule.id === id) {
schedules.splice(i, 1)
break
}
}
}
}
/**
* When `mock.time.add` is called, run schedules whose time has come.
*/
function runSchedules () {
// Sort by descending time order.
schedules.sort(function (a, b) {
return b.time - a.time
})
// Track the soonest interval run time, in case we're already there.
var minNewTime = Number.MAX_VALUE
// Iterate, from the end until we reach the current mock time.
var i = schedules.length - 1
var schedule = schedules[i]
while (schedule && (schedule.time <= mock.time._CURRENT_TIME)) {
schedule.fn()
// setTimeout schedules can be deleted.
if (!schedule.interval) {
schedules.splice(i, 1)
// setInterval schedules should schedule the next run.
} else {
schedule.time += schedule.interval
minNewTime = Math.min(minNewTime, schedule.time)
}
schedule = schedules[--i]
}
// If an interval schedule is in the past, catch it up.
if (minNewTime <= mock.time._CURRENT_TIME) {
process.nextTick(runSchedules)
}
}
/**
* Restore the real `Date` object and time-related functions.
*/
unmock.time = function () {
delete mock.time._CURRENT_TIME
delete mock.time._SPEED
global.Date = timers.Date
global.setTimeout = timers.setTimeout
global.setInterval = timers.setInterval
global.clearTimeout = timers.clearTimeout
global.clearInterval = timers.clearInterval
schedules.length = 0
unmock(timers.Date, 'now')
}
/**
* Expose `mock-fs` methods as exam/lib/mock methods.a
*/
methods = ['directory', 'file', 'symlink']
methods.forEach(function (method) {
mock[method] = function () {
var mockFs = require('mock-fs')
return mockFs[method].apply(mockFs, arguments)
}
})