diff options
author | Bao <baonhat.nguyen@gmail.com> | 2015-05-22 01:05:09 -0700 |
---|---|---|
committer | Bao <baonhat.nguyen@gmail.com> | 2015-05-22 01:05:09 -0700 |
commit | 807546d534786c1cb79a3f2ae2d4fa9d4863e6d0 (patch) | |
tree | 002d735fc5db2c7344353131fce2b2d7cba7a2d6 | |
parent | 11bf48b4ff52823edb66e35d05571724accc02d3 (diff) | |
parent | a44d11ca95304dca673e6487050e630c2b2d87ee (diff) | |
download | async-807546d534786c1cb79a3f2ae2d4fa9d4863e6d0.tar.gz |
Merge branch 'master' into fix/_eachLimit_continues_after_error
Conflicts:
lib/async.js
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | .jshintrc | 24 | ||||
-rw-r--r-- | .npmignore | 2 | ||||
-rw-r--r-- | .travis.yml | 3 | ||||
-rw-r--r-- | CHANGELOG.md | 16 | ||||
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | README.md | 104 | ||||
-rw-r--r-- | bower.json | 12 | ||||
-rw-r--r-- | component.json | 3 | ||||
-rw-r--r-- | lib/async.js | 336 | ||||
-rw-r--r-- | nodelint.cfg | 4 | ||||
-rw-r--r-- | package.json | 19 | ||||
-rwxr-xr-x | perf/benchmark.js | 192 | ||||
-rw-r--r-- | perf/suites.js | 209 | ||||
-rwxr-xr-x | support/sync-package-managers.js | 9 | ||||
-rwxr-xr-x | test/test-async.js | 495 |
16 files changed, 1214 insertions, 230 deletions
@@ -1,2 +1,3 @@ -/node_modules -/dist +node_modules +dist +perf/versions diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..172f491 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,24 @@ +{ + // Enforcing options + "eqeqeq": false, + "forin": true, + "indent": 4, + "noarg": true, + "undef": true, + "trailing": true, + "evil": true, + "laxcomma": true, + + // Relaxing options + "onevar": false, + "asi": false, + "eqnull": true, + "expr": false, + "loopfunc": true, + "sub": true, + "browser": true, + "node": true, + "globals": { + "define": true + } +} @@ -1,7 +1,7 @@ deps dist test -nodelint.cfg +perf .npmignore .gitmodules Makefile diff --git a/.travis.yml b/.travis.yml index 05d299e..6064ca0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: node_js node_js: - "0.10" - - "0.11" + - "0.12" + - "iojs" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d39c37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# v1.0.0 + +No known breaking changes, we are simply complying with semver from here on out. + +Changes: + +- Start using a changelog! +- Add `forEachOf` for iterating over Objects (or to iterate Arrays with indexes available) (#168 #704 #321) +- Detect deadlocks in `auto` (#663) +- Better support for require.js (#527) +- Throw if queue created with concurrency `0` (#714) +- Fix unneeded iteration in `queue.resume()` (#758) +- Guard against timer mocking overriding `setImmediate` (#609 #611) +- Miscellaneous doc fixes (#542 #596 #615 #628 #631 #690 #729) +- Use single noop function internally (#546) +- Optimize internal `_each`, `_map` and `_keys` functions. @@ -1,9 +1,8 @@ PACKAGE = asyncjs -NODEJS = $(if $(shell test -f /usr/bin/nodejs && echo "true"),nodejs,node) CWD := $(shell pwd) -NODEUNIT = $(CWD)/node_modules/nodeunit/bin/nodeunit -UGLIFY = $(CWD)/node_modules/uglify-js/bin/uglifyjs -NODELINT = $(CWD)/node_modules/nodelint/nodelint +NODEUNIT = "$(CWD)/node_modules/.bin/nodeunit" +UGLIFY = "$(CWD)/node_modules/.bin/uglifyjs" +JSHINT = "$(CWD)/node_modules/.bin/jshint" BUILDDIR = dist @@ -20,6 +19,6 @@ clean: rm -rf $(BUILDDIR) lint: - $(NODELINT) --config nodelint.cfg lib/async.js + $(JSHINT) lib/*.js test/*.js perf/*.js -.PHONY: test build all +.PHONY: test lint build all clean @@ -1,6 +1,7 @@ # Async.js [![Build Status via Travis CI](https://travis-ci.org/caolan/async.svg?branch=master)](https://travis-ci.org/caolan/async) +[![NPM version](http://img.shields.io/npm/v/async.svg)](https://www.npmjs.org/package/async) Async is a utility module which provides straight-forward, powerful functions @@ -88,11 +89,15 @@ async.map([1, 2, 3], AsyncSquaringLibrary.square.bind(AsyncSquaringLibrary), fun ## Download The source is available for download from -[GitHub](http://github.com/caolan/async). +[GitHub](https://github.com/caolan/async/blob/master/lib/async.js). Alternatively, you can install using Node Package Manager (`npm`): npm install async +As well as using Bower: + + bower install async + __Development:__ [async.js](https://github.com/caolan/async/raw/master/lib/async.js) - 29.6kb Uncompressed ## In the Browser @@ -119,6 +124,9 @@ Usage: * [`each`](#each) * [`eachSeries`](#eachSeries) * [`eachLimit`](#eachLimit) +* [`forEachOf`](#forEachOf) +* [`forEachOfSeries`](#forEachOfSeries) +* [`forEachOfLimit`](#forEachOfLimit) * [`map`](#map) * [`mapSeries`](#mapSeries) * [`mapLimit`](#mapLimit) @@ -191,7 +199,8 @@ __Arguments__ * `iterator(item, callback)` - A function to apply to each item in `arr`. The iterator is passed a `callback(err)` which must be called once it has completed. If no error has occurred, the `callback` should be run without - arguments or with an explicit `null` argument. + arguments or with an explicit `null` argument. The array index is not passed + to the iterator. If you need the index, use [`forEachOf`](#forEachOf). * `callback(err)` - A callback which is called when all `iterator` functions have finished, or an error occurs. @@ -282,13 +291,74 @@ async.eachLimit(documents, 20, requestApi, function(err){ --------------------------------------- +<a name="forEachOf" /> +<a name="eachOf" /> + +### forEachOf(obj, iterator, callback) + +Like `each`, except that it iterates over objects, and passes the key as the second argument to the iterator. + +__Arguments__ + +* `obj` - An object or array to iterate over. +* `iterator(item, key, callback)` - A function to apply to each item in `obj`. +The `key` is the item's key, or index in the case of an array. The iterator is +passed a `callback(err)` which must be called once it has completed. If no +error has occurred, the callback should be run without arguments or with an +explicit `null` argument. +* `callback(err)` - A callback which is called when all `iterator` functions have finished, or an error occurs. + +__Example__ + +```js +var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}; +var configs = {}; + +async.forEachOf(obj, function (value, key, callback) { + fs.readFile(__dirname + value, "utf8", function (err, data) { + if (err) return callback(err); + try { + configs[key] = JSON.parse(data); + } catch (e) { + return callback(e); + } + callback(); + }) +}, function (err) { + if (err) console.error(err.message); + // configs is now a map of JSON data + doSomethingWith(configs); +}) +``` + +--------------------------------------- + +<a name="forEachOfSeries" /> +<a name="eachOfSeries" /> + +### forEachOfSeries(obj, iterator, callback) + +Like [`forEachOf`](#forEachOf), except only one `iterator` is run at a time. The order of execution is not guaranteed for objects, but it will be guaranteed for arrays. + +--------------------------------------- + +<a name="forEachOfLimit" /> +<a name="eachOfLimit" /> + +### forEachOfLimit(obj, limit, iterator, callback) + +Like [`forEachOf`](#forEachOf), except the number of `iterator`s running at a given time is controlled by `limit`. + + +--------------------------------------- + <a name="map" /> ### map(arr, iterator, callback) Produces a new array of values by mapping each value in `arr` through the `iterator` function. The `iterator` is called with an item from `arr` and a callback for when it has finished processing. Each of these callback takes 2 arguments: -an `error`, and the transformed item from `arr`. If `iterator` passes an error to his +an `error`, and the transformed item from `arr`. If `iterator` passes an error to its callback, the main `callback` (for the `map` function) is immediately called with the error. Note, that since this function applies the `iterator` to each item in parallel, @@ -482,11 +552,11 @@ __Arguments__ * `arr` - An array to iterate over. * `iterator(item, callback)` - A truth test to apply to each item in `arr`. The iterator is passed a `callback(truthValue)` which must be called with a - boolean argument once it has completed. + boolean argument once it has completed. **Note: this callback does not take an error as its first argument.** * `callback(result)` - A callback which is called as soon as any iterator returns `true`, or after all the `iterator` functions have finished. Result will be the first item in the array that passes the truth test (iterator) or the - value `undefined` if none passed. + value `undefined` if none passed. **Note: this callback does not take an error as its first argument.** __Example__ @@ -574,12 +644,13 @@ __Arguments__ * `arr` - An array to iterate over. * `iterator(item, callback)` - A truth test to apply to each item in the array - in parallel. The iterator is passed a callback(truthValue) which must be + in parallel. The iterator is passed a `callback(truthValue)`` which must be called with a boolean argument once it has completed. * `callback(result)` - A callback which is called as soon as any iterator returns `true`, or after all the iterator functions have finished. Result will be either `true` or `false` depending on the values of the async tests. + **Note: the callbacks do not take an error as their first argument.** __Example__ ```js @@ -604,12 +675,14 @@ __Arguments__ * `arr` - An array to iterate over. * `iterator(item, callback)` - A truth test to apply to each item in the array - in parallel. The iterator is passed a callback(truthValue) which must be + in parallel. The iterator is passed a `callback(truthValue)` which must be called with a boolean argument once it has completed. * `callback(result)` - A callback which is called after all the `iterator` functions have finished. Result will be either `true` or `false` depending on the values of the async tests. + **Note: the callbacks do not take an error as their first argument.** + __Example__ ```js @@ -1048,14 +1121,14 @@ async.each( --------------------------------------- <a name="applyEachSeries" /> -### applyEachSeries(arr, iterator, callback) +### applyEachSeries(arr, args..., callback) The same as [`applyEach`](#applyEach) only the functions are applied in series. --------------------------------------- <a name="queue" /> -### queue(worker, concurrency) +### queue(worker, [concurrency]) Creates a `queue` object with the specified `concurrency`. Tasks added to the `queue` are processed in parallel (up to the `concurrency` limit). If all @@ -1066,9 +1139,9 @@ __Arguments__ * `worker(task, callback)` - An asynchronous function for processing a queued task, which must call its `callback(err)` argument when finished, with an - optional `error` as an argument. + optional `error` as an argument. If you want to handle errors from an individual task, pass a callback to `q.push()`. * `concurrency` - An `integer` for determining how many `worker` functions should be - run in parallel. + run in parallel. If omitted, the concurrency defaults to `1`. If the concurrency is `0`, an error is thrown. __Queue objects__ @@ -1154,7 +1227,7 @@ Creates a `cargo` object with the specified payload. Tasks added to the cargo will be processed altogether (up to the `payload` limit). If the `worker` is in progress, the task is queued until it becomes available. Once the `worker` has completed some tasks, each callback of those tasks is called. -Check out [this animation](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) for how `cargo` and `queue` work. +Check out [these](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) [animations](https://camo.githubusercontent.com/f4810e00e1c5f5f8addbe3e9f49064fd5d102699/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130312f38346339323036362d356632392d313165322d383134662d3964336430323431336266642e676966) for how `cargo` and `queue` work. While [queue](#queue) passes only one task to one of a group of workers at a time, cargo passes an array of tasks to a single worker, repeating @@ -1508,7 +1581,8 @@ you would use with [`map`](#map). __Arguments__ * `n` - The number of times to run the function. -* `callback` - The function to call `n` times. +* `iterator` - The function to call `n` times. +* `callback` - see [`map`](#map) __Example__ @@ -1546,13 +1620,15 @@ Caches the results of an `async` function. When creating a hash to store functio results against, the callback is omitted from the hash and an optional hash function can be used. +If no hash function is specified, the first argument is used as a hash key, which may work reasonably if it is a string or a data type that converts to a distinct string. Note that objects and arrays will not behave reasonably. Neither will cases where the other arguments are significant. In such cases, specify your own hash function. + The cache of results is exposed as the `memo` property of the function returned by `memoize`. __Arguments__ * `fn` - The function to proxy and cache results from. -* `hasher` - Tn optional function for generating a custom hash for storing +* `hasher` - An optional function for generating a custom hash for storing results. It has all the arguments applied to it apart from the callback, and must be synchronous. @@ -1,7 +1,7 @@ { "name": "async", "description": "Higher-order functions and common patterns for asynchronous code", - "version": "0.9.2", + "version": "1.0.0", "main": "lib/async.js", "keywords": [ "async", @@ -9,22 +9,24 @@ "utility", "module" ], + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/caolan/async.git" }, "devDependencies": { + "benchmark": "~1.0.0", + "jshint": "~2.7.0", + "lodash": ">=2.4.1", + "mkdirp": "~0.5.1", "nodeunit": ">0.0.0", - "uglify-js": "1.2.x", - "nodelint": ">0.0.0", - "lodash": ">=2.4.1" + "uglify-js": "1.2.x" }, "moduleType": [ "amd", "globals", "node" ], - "license": "MIT", "ignore": [ "**/.*", "node_modules", diff --git a/component.json b/component.json index 5003a7c..c876b0a 100644 --- a/component.json +++ b/component.json @@ -1,7 +1,7 @@ { "name": "async", "description": "Higher-order functions and common patterns for asynchronous code", - "version": "0.9.2", + "version": "1.0.0", "keywords": [ "async", "callback", @@ -9,6 +9,7 @@ "module" ], "license": "MIT", + "main": "lib/async.js", "repository": "caolan/async", "scripts": [ "lib/async.js" diff --git a/lib/async.js b/lib/async.js index b93e178..4257f0d 100644 --- a/lib/async.js +++ b/lib/async.js @@ -5,16 +5,24 @@ * Copyright 2010-2014 Caolan McMahon * Released under the MIT license */ -/*jshint onevar: false, indent:4 */ -/*global setImmediate: false, setTimeout: false, console: false */ (function () { var async = {}; + var noop = function () {}; // global on the server, window in the browser var root, previous_async; - root = this; + if (typeof window == 'object' && this === window) { + root = window; + } + else if (typeof global == 'object' && this === global) { + root = global; + } + else { + root = this; + } + if (root != null) { previous_async = root.async; } @@ -30,7 +38,7 @@ if (called) throw new Error("Callback was already called."); called = true; fn.apply(root, arguments); - } + }; } //// cross-browser compatiblity functions //// @@ -42,36 +50,39 @@ }; var _each = function (arr, iterator) { - for (var i = 0; i < arr.length; i += 1) { - iterator(arr[i], i, arr); - } + var index = -1, + length = arr.length; + + while (++index < length) { + iterator(arr[index], index, arr); + } }; var _map = function (arr, iterator) { - if (arr.map) { - return arr.map(iterator); - } - var results = []; - _each(arr, function (x, i, a) { - results.push(iterator(x, i, a)); - }); - return results; + var index = -1, + length = arr.length, + result = Array(length); + + while (++index < length) { + result[index] = iterator(arr[index], index, arr); + } + return result; }; var _reduce = function (arr, iterator, memo) { - if (arr.reduce) { - return arr.reduce(iterator, memo); - } _each(arr, function (x, i, a) { memo = iterator(memo, x, i, a); }); return memo; }; - var _keys = function (obj) { - if (Object.keys) { - return Object.keys(obj); - } + var _forEachOf = function (object, iterator) { + _each(_keys(object), function (key) { + iterator(object[key], key); + }); + }; + + var _keys = Object.keys || function (obj) { var keys = []; for (var k in obj) { if (obj.hasOwnProperty(k)) { @@ -81,14 +92,38 @@ return keys; }; + var _baseSlice = function (arr, start) { + start = start || 0; + var index = -1; + var length = arr.length; + + if (start) { + length -= start; + length = length < 0 ? 0 : length; + } + var result = Array(length); + + while (++index < length) { + result[index] = arr[index + start]; + } + return result; + }; + //// exported async module functions //// //// nextTick implementation with browser-compatible fallback //// + + // capture the global reference to guard against fakeTimer mocks + var _setImmediate; + if (typeof setImmediate === 'function') { + _setImmediate = setImmediate; + } + if (typeof process === 'undefined' || !(process.nextTick)) { - if (typeof setImmediate === 'function') { + if (_setImmediate) { async.nextTick = function (fn) { // not a direct alias for IE10 compatibility - setImmediate(fn); + _setImmediate(fn); }; async.setImmediate = async.nextTick; } @@ -101,10 +136,10 @@ } else { async.nextTick = process.nextTick; - if (typeof setImmediate !== 'undefined') { + if (_setImmediate) { async.setImmediate = function (fn) { // not a direct alias for IE10 compatibility - setImmediate(fn); + _setImmediate(fn); }; } else { @@ -113,9 +148,9 @@ } async.each = function (arr, iterator, callback) { - callback = callback || function () {}; + callback = callback || noop; if (!arr.length) { - return callback(); + return callback(null); } var completed = 0; _each(arr, function (x) { @@ -124,12 +159,12 @@ function done(err) { if (err) { callback(err); - callback = function () {}; + callback = noop; } else { completed += 1; if (completed >= arr.length) { - callback(); + callback(null); } } } @@ -137,21 +172,21 @@ async.forEach = async.each; async.eachSeries = function (arr, iterator, callback) { - callback = callback || function () {}; + callback = callback || noop; if (!arr.length) { - return callback(); + return callback(null); } var completed = 0; var iterate = function () { iterator(arr[completed], function (err) { if (err) { callback(err); - callback = function () {}; + callback = noop; } else { completed += 1; if (completed >= arr.length) { - callback(); + callback(null); } else { iterate(); @@ -163,6 +198,7 @@ }; async.forEachSeries = async.eachSeries; + async.eachLimit = function (arr, limit, iterator, callback) { var fn = _eachLimit(limit); fn.apply(null, [arr, iterator, callback]); @@ -172,9 +208,9 @@ var _eachLimit = function (limit) { return function (arr, iterator, callback) { - callback = callback || function () {}; + callback = callback || noop; if (!arr.length || limit <= 0) { - return callback(); + return callback(null); } var completed = 0; var started = 0; @@ -183,7 +219,7 @@ (function replenish () { if (completed >= arr.length) { - return callback(); + return callback(null); } while (running < limit && started < arr.length && !errored) { @@ -192,14 +228,123 @@ iterator(arr[started - 1], function (err) { if (err) { callback(err); - callback = function () {}; errored = true; + callback = noop; } else { completed += 1; running -= 1; if (completed >= arr.length) { - callback(); + callback(null); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + + async.forEachOf = async.eachOf = function (object, iterator, callback) { + callback = callback || function () {}; + var size = object.length || _keys(object).length; + var completed = 0; + if (!size) { + return callback(null); + } + _forEachOf(object, function (value, key) { + iterator(object[key], key, function (err) { + if (err) { + callback(err); + callback = function () {}; + } else { + completed += 1; + if (completed === size) { + callback(null); + } + } + }); + }); + }; + + async.forEachOfSeries = async.eachOfSeries = function (obj, iterator, callback) { + callback = callback || function () {}; + var keys = _keys(obj); + var size = keys.length; + if (!size) { + return callback(); + } + var completed = 0; + var iterate = function () { + var sync = true; + var key = keys[completed]; + iterator(obj[key], key, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= size) { + callback(null); + } + else { + if (sync) { + async.nextTick(iterate); + } + else { + iterate(); + } + } + } + }); + sync = false; + }; + iterate(); + }; + + + + async.forEachOfLimit = async.eachOfLimit = function (obj, limit, iterator, callback) { + _forEachOfLimit(limit)(obj, iterator, callback); + }; + + var _forEachOfLimit = function (limit) { + + return function (obj, iterator, callback) { + callback = callback || function () {}; + var keys = _keys(obj); + var size = keys.length; + if (!size || limit <= 0) { + return callback(null); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= size) { + return callback(); + } + + while (running < limit && started < size) { + started += 1; + running += 1; + var key = keys[started - 1]; + iterator(obj[key], key, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= size) { + callback(null); } else { replenish(); @@ -214,19 +359,19 @@ var doParallel = function (fn) { return function () { - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); return fn.apply(null, [async.each].concat(args)); }; }; var doParallelLimit = function(limit, fn) { return function () { - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); return fn.apply(null, [_eachLimit(limit)].concat(args)); }; }; var doSeries = function (fn) { return function () { - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); return fn.apply(null, [async.eachSeries].concat(args)); }; }; @@ -273,7 +418,7 @@ callback(err); }); }, function (err) { - callback(err, memo); + callback(err || null, memo); }); }; // inject alias @@ -344,7 +489,7 @@ iterator(x, function (result) { if (result) { main_callback(x); - main_callback = function () {}; + main_callback = noop; } else { callback(); @@ -362,7 +507,7 @@ iterator(x, function (v) { if (v) { main_callback(true); - main_callback = function () {}; + main_callback = noop; } callback(); }); @@ -378,7 +523,7 @@ iterator(x, function (v) { if (!v) { main_callback(false); - main_callback = function () {}; + main_callback = noop; } callback(); }); @@ -416,11 +561,11 @@ }; async.auto = function (tasks, callback) { - callback = callback || function () {}; + callback = callback || noop; var keys = _keys(tasks); - var remainingTasks = keys.length + var remainingTasks = keys.length; if (!remainingTasks) { - return callback(); + return callback(null); } var results = {}; @@ -438,7 +583,7 @@ } }; var taskComplete = function () { - remainingTasks-- + remainingTasks--; _each(listeners.slice(0), function (fn) { fn(); }); @@ -448,7 +593,7 @@ if (!remainingTasks) { var theCallback = callback; // prevent final callback from calling itself if it errors - callback = function () {}; + callback = noop; theCallback(null, results); } @@ -457,7 +602,7 @@ _each(keys, function (k) { var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; var taskCallback = function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (args.length <= 1) { args = args[0]; } @@ -469,7 +614,7 @@ safeResults[k] = args; callback(err, safeResults); // stop subsequent errors hitting callback multiple times - callback = function () {}; + callback = noop; } else { results[k] = args; @@ -477,6 +622,17 @@ } }; var requires = task.slice(0, Math.abs(task.length - 1)) || []; + // prevent dead-locks + var len = requires.length; + var dep; + while (len--) { + if (!(dep = tasks[requires[len]])) { + throw new Error('Has inexistant dependency'); + } + if (_isArray(dep) && !!~dep.indexOf(k)) { + throw new Error('Has cyclic dependencies'); + } + } var ready = function () { return _reduce(requires, function (a, x) { return (a && results.hasOwnProperty(x)); @@ -523,13 +679,13 @@ data = data[data.length - 1]; (wrappedCallback || callback)(data.err, data.result); }); - } + }; // If a callback is passed, run this as a controll flow - return callback ? wrappedTask() : wrappedTask + return callback ? wrappedTask() : wrappedTask; }; async.waterfall = function (tasks, callback) { - callback = callback || function () {}; + callback = callback || noop; if (!_isArray(tasks)) { var err = new Error('First argument to waterfall must be an array of functions'); return callback(err); @@ -541,10 +697,10 @@ return function (err) { if (err) { callback.apply(null, arguments); - callback = function () {}; + callback = noop; } else { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); var next = iterator.next(); if (next) { args.push(wrapIterator(next)); @@ -562,12 +718,12 @@ }; var _parallel = function(eachfn, tasks, callback) { - callback = callback || function () {}; + callback = callback || noop; if (_isArray(tasks)) { eachfn.map(tasks, function (fn, callback) { if (fn) { fn(function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (args.length <= 1) { args = args[0]; } @@ -580,7 +736,7 @@ var results = {}; eachfn.each(_keys(tasks), function (k, callback) { tasks[k](function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (args.length <= 1) { args = args[0]; } @@ -602,12 +758,12 @@ }; async.series = function (tasks, callback) { - callback = callback || function () {}; + callback = callback || noop; if (_isArray(tasks)) { async.mapSeries(tasks, function (fn, callback) { if (fn) { fn(function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (args.length <= 1) { args = args[0]; } @@ -620,7 +776,7 @@ var results = {}; async.eachSeries(_keys(tasks), function (k, callback) { tasks[k](function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (args.length <= 1) { args = args[0]; } @@ -650,10 +806,10 @@ }; async.apply = function (fn) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); return function () { return fn.apply( - null, args.concat(Array.prototype.slice.call(arguments)) + null, args.concat(_baseSlice(arguments)) ); }; }; @@ -682,7 +838,7 @@ }); } else { - callback(); + callback(null); } }; @@ -691,12 +847,12 @@ if (err) { return callback(err); } - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (test.apply(null, args)) { async.doWhilst(iterator, test, callback); } else { - callback(); + callback(null); } }); }; @@ -711,7 +867,7 @@ }); } else { - callback(); + callback(null); } }; @@ -720,12 +876,12 @@ if (err) { return callback(err); } - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (!test.apply(null, args)) { async.doUntil(iterator, test, callback); } else { - callback(); + callback(null); } }); }; @@ -734,6 +890,9 @@ if (concurrency === undefined) { concurrency = 1; } + else if(concurrency === 0) { + throw new Error('Concurrency must not be zero'); + } function _insert(q, data, pos, callback) { if (!q.started){ q.started = true; @@ -741,7 +900,7 @@ if (!_isArray(data)) { data = [data]; } - if(data.length == 0) { + if(data.length === 0) { // call drain immediately if there are no tasks return async.setImmediate(function() { if (q.drain) { @@ -824,9 +983,10 @@ resume: function () { if (q.paused === false) { return; } q.paused = false; + var resumeCount = Math.min(q.concurrency, q.tasks.length); // Need to call q.process once per concurrent // worker to preserve full concurrency after pause - for (var w = 1; w <= q.concurrency; w++) { + for (var w = 1; w <= resumeCount; w++) { async.setImmediate(q.process); } } @@ -838,7 +998,7 @@ function _compareTasks(a, b){ return a.priority - b.priority; - }; + } function _binarySearch(sequence, item, compare) { var beg = -1, @@ -861,7 +1021,7 @@ if (!_isArray(data)) { data = [data]; } - if(data.length == 0) { + if(data.length === 0) { // call drain immediately if there are no tasks return async.setImmediate(function() { if (q.drain) { @@ -934,9 +1094,9 @@ return; } - var ts = typeof payload === 'number' - ? tasks.splice(0, payload) - : tasks.splice(0, tasks.length); + var ts = typeof payload === 'number' ? + tasks.splice(0, payload) : + tasks.splice(0, tasks.length); var ds = _map(ts, function (task) { return task.data; @@ -969,9 +1129,9 @@ var _console_fn = function (name) { return function (fn) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); fn.apply(null, args.concat([function (err) { - var args = Array.prototype.slice.call(arguments, 1); + var args = _baseSlice(arguments, 1); if (typeof console !== 'undefined') { if (err) { if (console.error) { @@ -1000,7 +1160,7 @@ return x; }; var memoized = function () { - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); var callback = args.pop(); var key = hasher.apply(null, args); if (key in memo) { @@ -1014,7 +1174,7 @@ else { queues[key] = [callback]; fn.apply(null, args.concat([function () { - memo[key] = arguments; + memo[key] = _baseSlice(arguments); var q = queues[key]; delete queues[key]; for (var i = 0, l = q.length; i < l; i++) { @@ -1054,14 +1214,14 @@ var fns = arguments; return function () { var that = this; - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); var callback = args.pop(); async.reduce(fns, args, function (newargs, fn, cb) { fn.apply(that, newargs.concat([function () { var err = arguments[0]; - var nextargs = Array.prototype.slice.call(arguments, 1); + var nextargs = _baseSlice(arguments, 1); cb(err, nextargs); - }])) + }])); }, function (err, results) { callback.apply(that, [err].concat(results)); @@ -1076,7 +1236,7 @@ var _applyEach = function (eachfn, fns /*args...*/) { var go = function () { var that = this; - var args = Array.prototype.slice.call(arguments); + var args = _baseSlice(arguments); var callback = args.pop(); return eachfn(fns, function (fn, cb) { fn.apply(that, args.concat([cb])); @@ -1084,7 +1244,7 @@ callback); }; if (arguments.length > 2) { - var args = Array.prototype.slice.call(arguments, 2); + var args = _baseSlice(arguments, 2); return go.apply(this, args); } else { diff --git a/nodelint.cfg b/nodelint.cfg deleted file mode 100644 index 457a967..0000000 --- a/nodelint.cfg +++ /dev/null @@ -1,4 +0,0 @@ -var options = { - indent: 4, - onevar: false -}; diff --git a/package.json b/package.json index f5debe2..6e7282b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Higher-order functions and common patterns for asynchronous code", "main": "lib/async.js", "author": "Caolan McMahon", - "version": "0.9.2", + "version": "1.0.0", "keywords": [ "async", "callback", @@ -17,15 +17,15 @@ "bugs": { "url": "https://github.com/caolan/async/issues" }, - "license": { - "type": "MIT", - "url": "https://github.com/caolan/async/raw/master/LICENSE" - }, + "license": "MIT", "devDependencies": { + "benchmark": "bestiejs/benchmark.js", + "jshint": "~2.7.0", + "lodash": ">=2.4.1", + "mkdirp": "~0.5.1", "nodeunit": ">0.0.0", "uglify-js": "1.2.x", - "nodelint": ">0.0.0", - "lodash": ">=2.4.1" + "yargs": "~3.9.1" }, "jam": { "main": "lib/async.js", @@ -39,7 +39,8 @@ ] }, "scripts": { - "test": "nodeunit test/test-async.js" + "test": "npm run-script lint && nodeunit test/test-async.js", + "lint": "jshint lib/*.js test/*.js perf/*.js" }, "spm": { "main": "lib/async.js" @@ -54,4 +55,4 @@ "tests" ] } -}
\ No newline at end of file +} diff --git a/perf/benchmark.js b/perf/benchmark.js new file mode 100755 index 0000000..da59216 --- /dev/null +++ b/perf/benchmark.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +var _ = require("lodash"); +var Benchmark = require("benchmark"); +var benchOptions = {defer: true, minSamples: 1, maxTime: 2}; +var exec = require("child_process").exec; +var fs = require("fs"); +var path = require("path"); +var mkdirp = require("mkdirp"); +var async = require("../"); +var suiteConfigs = require("./suites"); + +var args = require("yargs") + .usage("Usage: $0 [options] [tag1] [tag2]") + .describe("g", "run only benchmarks whose names match this regex") + .alias("g", "grep") + .default("g", ".*") + .describe("i", "skip benchmarks whose names match this regex") + .alias("g", "reject") + .default("i", "^$") + .help('h') + .alias('h', 'help') + .example('$0 0.9.2 0.9.0', 'Compare v0.9.2 with v0.9.0') + .example('$0 0.9.2', 'Compare v0.9.2 with the current working version') + .example('$0', 'Compare the latest tag with the current working version') + .example('$0 -g each', 'only run the each(), eachLimit() and eachSeries() benchmarks') + .example('') + .argv; + +var grep = new RegExp(args.g, "i"); +var reject = new RegExp(args.i, "i"); + +var version0 = args._[0] || require("../package.json").version; +var version1 = args._[1] || "current"; +var versionNames = [version0, version1]; +var versions; +var wins = {}; +var totalTime = {}; +totalTime[version0] = wins[version0] = 0; +totalTime[version1] = wins[version1] = 0; + +console.log("Comparing " + version0 + " with " + version1); +console.log("--------------------------------------"); + + +async.eachSeries(versionNames, cloneVersion, function (err) { + versions = versionNames.map(requireVersion); + + var suites = suiteConfigs + .map(setDefaultOptions) + .reduce(handleMultipleArgs, []) + .map(setName) + .filter(matchesGrep) + .filter(doesNotMatch) + .map(createSuite); + + async.eachSeries(suites, runSuite, function () { + var totalTime0 = +totalTime[version0].toPrecision(3); + var totalTime1 = +totalTime[version1].toPrecision(3); + + var wins0 = wins[version0]; + var wins1 = wins[version1]; + + if ( Math.abs((totalTime0 / totalTime1) - 1) < 0.01) { + // if < 1% difference, we're likely within the margins of error + console.log("Both versions are about equal " + + "(" + totalTime0 + "ms total vs. " + totalTime1 + "ms total)"); + } else if (totalTime0 < totalTime1) { + console.log(version0 + " faster overall " + + "(" + totalTime0 + "ms total vs. " + totalTime1 + "ms total)"); + } else if (totalTime1 < totalTime0) { + console.log(version1 + " faster overall " + + "(" + totalTime1 + "ms total vs. " + totalTime0 + "ms total)"); + } + + if (wins0 > wins1) { + console.log(version0 + " won more benchmarks " + + "(" + wins0 + " vs. " + wins1 + ")"); + } else if (wins1 > wins0) { + console.log(version1 + " won more benchmarks " + + "(" + wins1 + " vs. " + wins0 + ")"); + } else { + console.log("Both versions won the same number of benchmarks " + + "(" + wins0 + " vs. " + wins1 + ")"); + } + }); +}); + +function runSuite(suite, callback) { + suite.on("complete", function () { + callback(); + }).run({async: true}); +} + +function setDefaultOptions(suiteConfig) { + suiteConfig.args = suiteConfig.args || [[]]; + suiteConfig.setup = suiteConfig.setup || function () {}; + return suiteConfig; +} + +function handleMultipleArgs(list, suiteConfig) { + return list.concat(suiteConfig.args.map(function (args) { + return _.defaults({args: args}, suiteConfig); + })); +} + +function setName(suiteConfig) { + suiteConfig.name = suiteConfig.name + "(" + suiteConfig.args.join(",") + ")"; + return suiteConfig; +} + +function matchesGrep(suiteConfig) { + return !!grep.exec(suiteConfig.name); +} + +function doesNotMatch(suiteConfig) { + return !reject.exec(suiteConfig.name); +} + +function createSuite(suiteConfig) { + var suite = new Benchmark.Suite(); + var args = suiteConfig.args; + + function addBench(version, versionName) { + var name = suiteConfig.name + " " + versionName; + suite.add(name, function (deferred) { + suiteConfig.fn(version, function () { + deferred.resolve(); + }); + }, _.extend({ + versionName: versionName, + setup: _.partial.apply(null, [suiteConfig.setup].concat(args)) + }, benchOptions)); + } + + addBench(versions[0], versionNames[0]); + addBench(versions[1], versionNames[1]); + + + return suite.on('cycle', function(event) { + var mean = event.target.stats.mean * 1000; + console.log(event.target + ", " + (+mean.toPrecision(2)) + "ms per run"); + var version = event.target.options.versionName; + totalTime[version] += mean; + }) + .on('complete', function() { + var fastest = this.filter('fastest'); + if (fastest.length === 2) { + console.log("Tie"); + } else { + var winner = fastest[0].options.versionName; + console.log(winner + ' is faster'); + wins[winner]++; + } + console.log("--------------------------------------"); + }); + +} + +function requireVersion(tag) { + if (tag === "current") { + return async; + } + + return require("./versions/" + tag + "/"); +} + +function cloneVersion(tag, callback) { + if (tag === "current") return callback(); + + var versionDir = __dirname + "/versions/" + tag; + mkdirp.sync(versionDir); + fs.open(versionDir + "/package.json", "r", function (err, handle) { + if (!err) { + // version has already been cloned + fs.close(handle); + return callback(); + } + + var repoPath = path.join(__dirname, ".."); + + var cmd = "git clone --branch " + tag + " " + repoPath + " " + versionDir; + + exec(cmd, function (err, stdout, stderr) { + if (err) { + throw err; + } + callback(); + }); + + }); +} diff --git a/perf/suites.js b/perf/suites.js new file mode 100644 index 0000000..9a36db5 --- /dev/null +++ b/perf/suites.js @@ -0,0 +1,209 @@ +var _ = require("lodash"); +var tasks; + +module.exports = [ + { + name: "each", + // args lists are passed to the setup function + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.each(tasks, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "eachSeries", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.eachSeries(tasks, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "eachLimit", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.eachLimit(tasks, 4, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "map", + // args lists are passed to the setup function + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.map(tasks, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "mapSeries", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.mapSeries(tasks, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "mapLimit", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.mapLimit(tasks, 4, function (num, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "eachOf", + // args lists are passed to the setup function + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.eachOf(tasks, function (num, i, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "eachOfSeries", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.eachOfSeries(tasks, function (num, i, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "eachOfLimit", + args: [[10], [300], [10000]], + setup: function(count) { + tasks = _.range(count); + }, + fn: function (async, done) { + async.eachOfLimit(tasks, 4, function (num, i, cb) { + async.setImmediate(cb); + }, done); + } + }, + { + name: "parallel", + args: [[10], [100], [1000]], + setup: function (count) { + tasks = _.range(count).map(function () { + return function (cb) { + setImmediate(cb); + }; + }); + }, + fn: function (async, done) { + async.parallel(tasks, done); + } + }, + { + name: "series", + args: [[10], [100], [1000]], + setup: function (count) { + tasks = _.range(count).map(function () { + return function (cb) { setImmediate(cb); }; + }); + }, + fn: function (async, done) { + async.series(tasks, done); + } + }, + { + name: "waterfall", + args: [[10], [100], [1000]], + setup: function (count) { + tasks = [ + function (cb) { + return cb(null, 1); + } + ].concat(_.range(count).map(function (i) { + return function (arg, cb) { cb(null, i); }; + })); + }, + fn: function (async, done) { + async.waterfall(tasks, done); + } + }, + { + name: "queue", + args: [[1000], [30000], [100000], [200000]], + setup: function (count) { + tasks = count; + }, + fn: function (async, done) { + var numEntries = tasks; + var q = async.queue(worker, 1); + for (var i = 1; i <= numEntries; i++) { + q.push({num: i}); + } + function worker(task, callback) { + if (task.num === numEntries) { + return done(); + } + setImmediate(callback); + } + } + }, + { + name: "defer nextTick", + fn: function (async, done) { + process.nextTick(done); + } + }, + { + name: "defer setImmediate", + fn: function (async, done) { + setImmediate(done); + } + }, + { + name: "defer async.nextTick", + fn: function (async, done) { + async.nextTick(done); + } + }, + { + name: "defer async.setImmediate", + fn: function (async, done) { + async.setImmediate(done); + } + }, + { + name: "defer setTimeout", + fn: function (async, done) { + setTimeout(done, 0); + } + } +]; + diff --git a/support/sync-package-managers.js b/support/sync-package-managers.js index 310bbe6..5b26119 100755 --- a/support/sync-package-managers.js +++ b/support/sync-package-managers.js @@ -13,8 +13,6 @@ var IGNORES = ['**/.*', 'node_modules', 'bower_components', 'test', 'tests']; var INCLUDES = ['lib/async.js', 'README.md', 'LICENSE']; var REPOSITORY_NAME = 'caolan/async'; -var LICENSE_NAME = packageJson.license.type; - packageJson.jam = { main: packageJson.main, include: INCLUDES, @@ -32,21 +30,20 @@ packageJson.volo = { var bowerSpecific = { moduleType: ['amd', 'globals', 'node'], - license: LICENSE_NAME, ignore: IGNORES, authors: [packageJson.author] }; var bowerInclude = ['name', 'description', 'version', 'main', 'keywords', - 'homepage', 'repository', 'devDependencies']; + 'license', 'homepage', 'repository', 'devDependencies']; var componentSpecific = { - license: LICENSE_NAME, repository: REPOSITORY_NAME, scripts: [packageJson.main] }; -var componentInclude = ['name', 'description', 'version', 'keywords']; +var componentInclude = ['name', 'description', 'version', 'keywords', + 'license', 'main']; var bowerJson = _.merge({}, _.pick(packageJson, bowerInclude), bowerSpecific); var componentJson = _.merge({}, _.pick(packageJson, componentInclude), componentSpecific); diff --git a/test/test-async.js b/test/test-async.js index 2d46b8c..1cb860e 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -6,7 +6,7 @@ if (!Function.prototype.bind) { var self = this; return function () { self.apply(thisArg, args.concat(Array.prototype.slice.call(arguments))); - } + }; }; } @@ -17,6 +17,13 @@ function eachIterator(args, x, callback) { }, x*25); } +function forEachOfIterator(args, value, key, callback) { + setTimeout(function(){ + args.push(key, value); + callback(); + }, value*25); +} + function mapIterator(call_order, x, callback) { setTimeout(function(){ call_order.push(x); @@ -43,6 +50,13 @@ function eachNoCallbackIterator(test, x, callback) { test.done(); } +function forEachOfNoCallbackIterator(test, x, key, callback) { + test.equal(x, 1); + test.equal(key, "a"); + callback(); + test.done(); +} + function getFunctionsObject(call_order) { return { one: function(callback){ @@ -85,7 +99,7 @@ exports['forever'] = function (test) { }; exports['applyEach'] = function (test) { - test.expect(4); + test.expect(5); var call_order = []; var one = function (val, cb) { test.equal(val, 5); @@ -109,13 +123,14 @@ exports['applyEach'] = function (test) { }, 150); }; async.applyEach([one, two, three], 5, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, ['two', 'one', 'three']); test.done(); }); }; exports['applyEachSeries'] = function (test) { - test.expect(4); + test.expect(5); var call_order = []; var one = function (val, cb) { test.equal(val, 5); @@ -139,6 +154,7 @@ exports['applyEachSeries'] = function (test) { }, 150); }; async.applyEachSeries([one, two, three], 5, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, ['one', 'two', 'three']); test.done(); }); @@ -175,7 +191,7 @@ exports['applyEach partial application'] = function (test) { }; exports['compose'] = function (test) { - test.expect(4); + test.expect(5); var add2 = function (n, cb) { test.equal(n, 3); setTimeout(function () { @@ -199,6 +215,7 @@ exports['compose'] = function (test) { if (err) { return test.done(err); } + test.ok(err === null, err + " passed instead of 'null'"); test.equal(result, 16); test.done(); }); @@ -262,7 +279,7 @@ exports['compose binding'] = function (test) { }; exports['seq'] = function (test) { - test.expect(4); + test.expect(5); var add2 = function (n, cb) { test.equal(n, 3); setTimeout(function () { @@ -286,6 +303,7 @@ exports['seq'] = function (test) { if (err) { return test.done(err); } + test.ok(err === null, err + " passed instead of 'null'"); test.equal(result, 16); test.done(); }); @@ -384,6 +402,7 @@ exports['auto'] = function(test){ }] }, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(callOrder, ['task2','task6','task3','task5','task1','task4']); test.done(); }); @@ -457,6 +476,7 @@ exports['auto results'] = function(test){ exports['auto empty object'] = function(test){ async.auto({}, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.done(); }); }; @@ -512,7 +532,7 @@ exports['auto error should pass partial results'] = function(test) { // Issue 76 on github: https://github.com/caolan/async/issues#issue/76 exports['auto removeListener has side effect on loop iterator'] = function(test) { async.auto({ - task1: ['task3', function(callback) { test.done() }], + task1: ['task3', function(callback) { test.done(); }], task2: ['task3', function(callback) { /* by design: DON'T call callback */ }], task3: function(callback) { callback(); } }); @@ -559,7 +579,7 @@ exports['auto calls callback multiple times'] = function(test) { exports['auto modifying results causes final callback to run early'] = function(test) { async.auto({ task1: function(callback, results){ - results.inserted = true + results.inserted = true; callback(null, 'task1'); }, task2: function(callback){ @@ -574,26 +594,54 @@ exports['auto modifying results causes final callback to run early'] = function( } }, function(err, results){ - test.equal(results.inserted, true) - test.ok(results.task3, 'task3') + test.equal(results.inserted, true); + test.ok(results.task3, 'task3'); test.done(); }); }; +// Issue 263 on github: https://github.com/caolan/async/issues/263 +exports['auto prevent dead-locks due to inexistant dependencies'] = function(test) { + test.throws(function () { + async.auto({ + task1: ['noexist', function(callback, results){ + callback(null, 'task1'); + }] + }); + }, Error); + test.done(); +}; + +// Issue 263 on github: https://github.com/caolan/async/issues/263 +exports['auto prevent dead-locks due to cyclic dependencies'] = function(test) { + test.throws(function () { + async.auto({ + task1: ['task2', function(callback, results){ + callback(null, 'task1'); + }], + task2: ['task1', function(callback, results){ + callback(null, 'task2'); + }] + }); + }, Error); + test.done(); +}; + // Issue 306 on github: https://github.com/caolan/async/issues/306 exports['retry when attempt succeeds'] = function(test) { - var failed = 3 - var callCount = 0 - var expectedResult = 'success' + var failed = 3; + var callCount = 0; + var expectedResult = 'success'; function fn(callback, results) { - callCount++ - failed-- - if (!failed) callback(null, expectedResult) - else callback(true) // respond with error + callCount++; + failed--; + if (!failed) callback(null, expectedResult); + else callback(true); // respond with error } async.retry(fn, function(err, result){ - test.equal(callCount, 3, 'did not retry the correct number of times') - test.equal(result, expectedResult, 'did not return the expected result') + test.ok(err === null, err + " passed instead of 'null'"); + test.equal(callCount, 3, 'did not retry the correct number of times'); + test.equal(result, expectedResult, 'did not return the expected result'); test.done(); }); }; @@ -606,7 +654,7 @@ exports['retry when all attempts succeeds'] = function(test) { function fn(callback, results) { callCount++; callback(error + callCount, erroredResult + callCount); // respond with indexed values - }; + } async.retry(times, fn, function(err, result){ test.equal(callCount, 3, "did not retry the correct number of times"); test.equal(err, error + times, "Incorrect error was returned"); @@ -619,7 +667,7 @@ exports['retry as an embedded task'] = function(test) { var retryResult = 'RETRY'; var fooResults; var retryResults; - + async.auto({ foo: function(callback, results){ fooResults = results; @@ -636,8 +684,10 @@ exports['retry as an embedded task'] = function(test) { }); }; -exports['waterfall'] = function(test){ - test.expect(6); +exports['waterfall'] = { + +'basic': function(test){ + test.expect(7); var call_order = []; async.waterfall([ function(callback){ @@ -663,31 +713,32 @@ exports['waterfall'] = function(test){ callback(null, 'test'); } ], function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.done(); }); -}; +}, -exports['waterfall empty array'] = function(test){ +'empty array': function(test){ async.waterfall([], function(err){ test.done(); }); -}; +}, -exports['waterfall non-array'] = function(test){ +'non-array': function(test){ async.waterfall({}, function(err){ test.equals(err.message, 'First argument to waterfall must be an array of functions'); test.done(); }); -}; +}, -exports['waterfall no callback'] = function(test){ +'no callback': function(test){ async.waterfall([ function(callback){callback();}, function(callback){callback(); test.done();} ]); -}; +}, -exports['waterfall async'] = function(test){ +'async': function(test){ var call_order = []; async.waterfall([ function(callback){ @@ -704,9 +755,9 @@ exports['waterfall async'] = function(test){ test.done(); } ]); -}; +}, -exports['waterfall error'] = function(test){ +'error': function(test){ test.expect(1); async.waterfall([ function(callback){ @@ -720,9 +771,9 @@ exports['waterfall error'] = function(test){ test.equals(err, 'error'); }); setTimeout(test.done, 50); -}; +}, -exports['waterfall multiple callback calls'] = function(test){ +'multiple callback calls': function(test){ var call_order = []; var arr = [ function(callback){ @@ -749,9 +800,9 @@ exports['waterfall multiple callback calls'] = function(test){ } ]; async.waterfall(arr); -}; +}, -exports['waterfall call in another context'] = function(test) { +'call in another context': function(test) { if (typeof process === 'undefined') { // node only test test.done(); @@ -776,8 +827,9 @@ exports['waterfall call in another context'] = function(test) { }).toString() + "())"; vm.runInNewContext(fn, sandbox); -}; +} +}; exports['parallel'] = function(test){ var call_order = []; @@ -802,7 +854,7 @@ exports['parallel'] = function(test){ } ], function(err, results){ - test.equals(err, null); + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [3,1,2]); test.same(results, [1,2,[3,3]]); test.done(); @@ -811,7 +863,7 @@ exports['parallel'] = function(test){ exports['parallel empty array'] = function(test){ async.parallel([], function(err, results){ - test.equals(err, null); + test.ok(err === null, err + " passed instead of 'null'"); test.same(results, []); test.done(); }); @@ -877,7 +929,7 @@ exports['parallel limit'] = function(test){ ], 2, function(err, results){ - test.equals(err, null); + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [1,3,2]); test.same(results, [1,2,[3,3]]); test.done(); @@ -886,7 +938,7 @@ exports['parallel limit'] = function(test){ exports['parallel limit empty array'] = function(test){ async.parallelLimit([], 2, function(err, results){ - test.equals(err, null); + test.ok(err === null, err + " passed instead of 'null'"); test.same(results, []); test.done(); }); @@ -979,7 +1031,7 @@ exports['series'] = function(test){ } ], function(err, results){ - test.equals(err, null); + test.ok(err === null, err + " passed instead of 'null'"); test.same(results, [1,2,[3,3]]); test.same(call_order, [1,2,3]); test.done(); @@ -1118,6 +1170,7 @@ exports['iterator.next'] = function(test){ exports['each'] = function(test){ var args = []; async.each([1,3,2], eachIterator.bind(this, args), function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(args, [1,2,3]); test.done(); }); @@ -1165,9 +1218,52 @@ exports['forEach alias'] = function (test) { test.done(); }; +exports['forEachOf'] = function(test){ + var args = []; + async.forEachOf({ a: 1, b: 2 }, forEachOfIterator.bind(this, args), function(err){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(args, ["a", 1, "b", 2]); + test.done(); + }); +}; + +exports['forEachOf empty object'] = function(test){ + test.expect(1); + async.forEachOf({}, function(value, key, callback){ + test.ok(false, 'iterator should not be called'); + callback(); + }, function(err) { + test.ok(true, 'should call callback'); + }); + setTimeout(test.done, 25); +}; + +exports['forEachOf error'] = function(test){ + test.expect(1); + async.forEachOf({ a: 1, b: 2 }, function(value, key, callback) { + callback('error'); + }, function(err){ + test.equals(err, 'error'); + }); + setTimeout(test.done, 50); +}; + +exports['forEachOf no callback'] = function(test){ + async.forEachOf({ a: 1 }, forEachOfNoCallbackIterator.bind(this, test)); +}; + +exports['forEachOf with array'] = function(test){ + var args = []; + async.forEachOf([ "a", "b" ], forEachOfIterator.bind(this, args), function(err){ + test.same(args, [0, "a", 1, "b"]); + test.done(); + }); +}; + exports['eachSeries'] = function(test){ var args = []; async.eachSeries([1,3,2], eachIterator.bind(this, args), function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(args, [1,3,2]); test.done(); }); @@ -1201,10 +1297,6 @@ exports['eachSeries no callback'] = function(test){ async.eachSeries([1], eachNoCallbackIterator.bind(this, test)); }; -exports['forEachSeries alias'] = function (test) { - test.strictEqual(async.eachSeries, async.forEachSeries); - test.done(); -}; exports['eachLimit'] = function(test){ var args = []; @@ -1215,6 +1307,7 @@ exports['eachLimit'] = function(test){ callback(); }, x*5); }, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(args, arr); test.done(); }); @@ -1293,14 +1386,159 @@ exports['eachLimit synchronous'] = function(test){ }); }; +exports['forEachSeries alias'] = function (test) { + test.strictEqual(async.eachSeries, async.forEachSeries); + test.done(); +}; + +exports['forEachOfSeries'] = function(test){ + var args = []; + async.forEachOfSeries({ a: 1, b: 2 }, forEachOfIterator.bind(this, args), function(err){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(args, [ "a", 1, "b", 2 ]); + test.done(); + }); +}; + +exports['forEachOfSeries empty object'] = function(test){ + test.expect(1); + async.forEachOfSeries({}, function(x, callback){ + test.ok(false, 'iterator should not be called'); + callback(); + }, function(err){ + test.ok(true, 'should call callback'); + }); + setTimeout(test.done, 25); +}; + +exports['forEachOfSeries error'] = function(test){ + test.expect(2); + var call_order = []; + async.forEachOfSeries({ a: 1, b: 2 }, function(value, key, callback){ + call_order.push(value, key); + callback('error'); + }, function(err){ + test.same(call_order, [ 1, "a" ]); + test.equals(err, 'error'); + }); + setTimeout(test.done, 50); +}; + +exports['forEachOfSeries no callback'] = function(test){ + async.forEachOfSeries({ a: 1 }, forEachOfNoCallbackIterator.bind(this, test)); +}; + +exports['forEachOfSeries with array'] = function(test){ + var args = []; + async.forEachOfSeries([ "a", "b" ], forEachOfIterator.bind(this, args), function(err){ + test.same(args, [ 0, "a", 1, "b" ]); + test.done(); + }); +}; + exports['forEachLimit alias'] = function (test) { test.strictEqual(async.eachLimit, async.forEachLimit); test.done(); }; +exports['forEachOfLimit'] = function(test){ + var args = []; + var obj = { a: 1, b: 2, c: 3, d: 4 }; + async.forEachOfLimit(obj, 2, function(value, key, callback){ + setTimeout(function(){ + args.push(value, key); + callback(); + }, value * 5); + }, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(args, [ 1, "a", 2, "b", 3, "c", 4, "d" ]); + test.done(); + }); +}; + +exports['forEachOfLimit empty object'] = function(test){ + test.expect(1); + async.forEachOfLimit({}, 2, function(value, key, callback){ + test.ok(false, 'iterator should not be called'); + callback(); + }, function(err){ + test.ok(true, 'should call callback'); + }); + setTimeout(test.done, 25); +}; + +exports['forEachOfLimit limit exceeds size'] = function(test){ + var args = []; + var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + async.forEachOfLimit(obj, 10, forEachOfIterator.bind(this, args), function(err){ + test.same(args, [ "a", 1, "b", 2, "c", 3, "d", 4, "e", 5 ]); + test.done(); + }); +}; + +exports['forEachOfLimit limit equal size'] = function(test){ + var args = []; + var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + async.forEachOfLimit(obj, 5, forEachOfIterator.bind(this, args), function(err){ + test.same(args, [ "a", 1, "b", 2, "c", 3, "d", 4, "e", 5 ]); + test.done(); + }); +}; + +exports['forEachOfLimit zero limit'] = function(test){ + test.expect(1); + async.forEachOfLimit({ a: 1, b: 2 }, 0, function(x, callback){ + test.ok(false, 'iterator should not be called'); + callback(); + }, function(err){ + test.ok(true, 'should call callback'); + }); + setTimeout(test.done, 25); +}; + +exports['forEachOfLimit error'] = function(test){ + test.expect(2); + var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + var call_order = []; + + async.forEachOfLimit(obj, 3, function(value, key, callback){ + call_order.push(value, key); + if (value === 2) { + callback('error'); + } + }, function(err){ + test.same(call_order, [ 1, "a", 2, "b" ]); + test.equals(err, 'error'); + }); + setTimeout(test.done, 25); +}; + +exports['forEachOfLimit no callback'] = function(test){ + async.forEachOfLimit({ a: 1 }, 1, forEachOfNoCallbackIterator.bind(this, test)); +}; + +exports['forEachOfLimit synchronous'] = function(test){ + var args = []; + var obj = { a: 1, b: 2 }; + async.forEachOfLimit(obj, 5, forEachOfIterator.bind(this, args), function(err){ + test.same(args, [ "a", 1, "b", 2 ]); + test.done(); + }); +}; + +exports['forEachOfLimit with array'] = function(test){ + var args = []; + var arr = [ "a", "b" ]; + async.forEachOfLimit(arr, 1, forEachOfIterator.bind(this, args), function (err) { + test.same(args, [ 0, "a", 1, "b" ]); + test.done(); + }); +}; + exports['map'] = function(test){ var call_order = []; async.map([1,3,2], mapIterator.bind(this, call_order), function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [1,2,3]); test.same(results, [2,6,4]); test.done(); @@ -1344,6 +1582,7 @@ exports['map error'] = function(test){ exports['mapSeries'] = function(test){ var call_order = []; async.mapSeries([1,3,2], mapIterator.bind(this, call_order), function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [1,3,2]); test.same(results, [2,6,4]); test.done(); @@ -1364,6 +1603,7 @@ exports['mapSeries error'] = function(test){ exports['mapLimit'] = function(test){ var call_order = []; async.mapLimit([2,4,3], 2, mapIterator.bind(this, call_order), function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [2,4,3]); test.same(results, [4,8,6]); test.done(); @@ -1435,6 +1675,7 @@ exports['reduce'] = function(test){ call_order.push(x); callback(null, a + x); }, function(err, result){ + test.ok(err === null, err + " passed instead of 'null'"); test.equals(result, 6); test.same(call_order, [1,2,3]); test.done(); @@ -1443,7 +1684,7 @@ exports['reduce'] = function(test){ exports['reduce async with non-reference memo'] = function(test){ async.reduce([1,3,2], 0, function(a, x, callback){ - setTimeout(function(){callback(null, a + x)}, Math.random()*100); + setTimeout(function(){callback(null, a + x);}, Math.random()*100); }, function(err, result){ test.equals(result, 6); test.done(); @@ -1675,10 +1916,21 @@ exports['detectSeries - multiple matches'] = function(test){ }, 200); }; +exports['detectSeries - ensure stop'] = function (test) { + async.detectSeries([1, 2, 3, 4, 5], function (num, cb) { + if (num > 3) throw new Error("detectSeries did not stop iterating"); + cb(num === 3); + }, function (result) { + test.equals(result, 3); + test.done(); + }); +}; + exports['sortBy'] = function(test){ async.sortBy([{a:1},{a:15},{a:6}], function(x, callback){ setTimeout(function(){callback(null, x.a);}, 0); }, function(err, result){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(result, [{a:1},{a:6},{a:15}]); test.done(); }); @@ -1696,7 +1948,7 @@ exports['sortBy inverted'] = function(test){ exports['apply'] = function(test){ test.expect(6); var fn = function(){ - test.same(Array.prototype.slice.call(arguments), [1,2,3,4]) + test.same(Array.prototype.slice.call(arguments), [1,2,3,4]); }; async.apply(fn, 1, 2, 3, 4)(); async.apply(fn, 1, 2, 3)(4); @@ -1704,7 +1956,7 @@ exports['apply'] = function(test){ async.apply(fn, 1)(2, 3, 4); async.apply(fn)(1, 2, 3, 4); test.equals( - async.apply(function(name){return 'hello ' + name}, 'world')(), + async.apply(function(name){return 'hello ' + name;}, 'world')(), 'hello world' ); test.done(); @@ -1774,14 +2026,15 @@ var console_fn_tests = function(name){ exports['times'] = function(test) { - var indices = [] + var indices = []; async.times(5, function(n, next) { - next(null, n) + next(null, n); }, function(err, results) { - test.same(results, [0,1,2,3,4]) - test.done() - }) -} + test.ok(err === null, err + " passed instead of 'null'"); + test.same(results, [0,1,2,3,4]); + test.done(); + }); +}; exports['times'] = function(test){ var args = []; @@ -1869,9 +2122,6 @@ exports['nextTick in the browser'] = function(test){ call_order.push('one'); setTimeout(function(){ - if (typeof process !== 'undefined') { - process.nextTick = _nextTick; - } test.same(call_order, ['one','two']); }, 50); setTimeout(test.done, 100); @@ -1924,7 +2174,7 @@ exports['concat'] = function(test){ async.concat([1,3,2], iterator, function(err, results){ test.same(results, [1,2,1,3,2,1]); test.same(call_order, [1,2,3]); - test.ok(!err); + test.ok(err === null, err + " passed instead of 'null'"); test.done(); }); }; @@ -1955,7 +2205,7 @@ exports['concatSeries'] = function(test){ async.concatSeries([1,3,2], iterator, function(err, results){ test.same(results, [1,3,2,1,2,1]); test.same(call_order, [1,3,2]); - test.ok(!err); + test.ok(err === null, err + " passed instead of 'null'"); test.done(); }); }; @@ -1975,6 +2225,7 @@ exports['until'] = function (test) { cb(); }, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [ ['test', 0], ['iterator', 0], ['test', 1], @@ -2003,6 +2254,7 @@ exports['doUntil'] = function (test) { return (count == 5); }, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [ ['iterator', 0], ['test', 1], ['iterator', 1], ['test', 2], @@ -2058,6 +2310,7 @@ exports['whilst'] = function (test) { cb(); }, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [ ['test', 0], ['iterator', 0], ['test', 1], @@ -2087,6 +2340,7 @@ exports['doWhilst'] = function (test) { return (count < 5); }, function (err) { + test.ok(err === null, err + " passed instead of 'null'"); test.same(call_order, [ ['iterator', 0], ['test', 1], ['iterator', 1], ['test', 2], @@ -2236,6 +2490,13 @@ exports['queue default concurrency'] = function (test) { }; }; +exports['queue zero concurrency'] = function(test){ + test.throws(function () { + async.queue(function (task, callback) {}, 0); + }); + test.done(); +}; + exports['queue error propagation'] = function(test){ var results = []; @@ -2423,12 +2684,12 @@ exports['queue bulk task'] = function (test) { exports['queue idle'] = function(test) { var q = async.queue(function (task, callback) { // Queue is busy when workers are running - test.equal(q.idle(), false) + test.equal(q.idle(), false); callback(); }, 1); // Queue is idle before anything added - test.equal(q.idle(), true) + test.equal(q.idle(), true); q.unshift(4); q.unshift(3); @@ -2436,14 +2697,14 @@ exports['queue idle'] = function(test) { q.unshift(1); // Queue is busy when tasks added - test.equal(q.idle(), false) + test.equal(q.idle(), false); q.drain = function() { // Queue is idle after drain test.equal(q.idle(), true); test.done(); - } -} + }; +}; exports['queue pause'] = function(test) { var call_order = [], @@ -2496,7 +2757,7 @@ exports['queue pause'] = function(test) { ]); test.done(); }, 800); -} +}; exports['queue pause with concurrency'] = function(test) { var call_order = [], @@ -2533,6 +2794,10 @@ exports['queue pause with concurrency'] = function(test) { }, resume_timeout); setTimeout(function () { + test.equal(q.running(), 2); + }, resume_timeout + 10); + + setTimeout(function () { test.same(call_order, [ 'process 1', 'timeout 100', 'process 2', 'timeout 100', @@ -2543,7 +2808,31 @@ exports['queue pause with concurrency'] = function(test) { ]); test.done(); }, 800); -} +}; + +exports['queue start paused'] = function (test) { + var q = async.queue(function (task, callback) { + setTimeout(function () { + callback(); + }, 10); + }, 2); + q.pause(); + + q.push([1, 2, 3]); + + setTimeout(function () { + q.resume(); + }, 10); + + setTimeout(function () { + test.equal(q.running(), 2); + q.resume(); + }, 15); + + q.drain = function () { + test.done(); + }; +}; exports['queue kill'] = function (test) { var q = async.queue(function (task, callback) { @@ -2554,7 +2843,7 @@ exports['queue kill'] = function (test) { }, 1); q.drain = function() { test.ok(false, "Function should never be called"); - } + }; q.push(0); @@ -2563,7 +2852,7 @@ exports['queue kill'] = function (test) { setTimeout(function() { test.equal(q.length(), 0); test.done(); - }, 600) + }, 600); }; exports['priorityQueue'] = function (test) { @@ -2806,20 +3095,20 @@ exports['cargo bulk task'] = function (test) { }; exports['cargo drain once'] = function (test) { - + var c = async.cargo(function (tasks, callback) { callback(); }, 3); - + var drainCounter = 0; c.drain = function () { drainCounter++; - } - + }; + for(var i = 0; i < 10; i++){ c.push(i); } - + setTimeout(function(){ test.equal(drainCounter, 1); test.done(); @@ -2827,21 +3116,21 @@ exports['cargo drain once'] = function (test) { }; exports['cargo drain twice'] = function (test) { - + var c = async.cargo(function (tasks, callback) { callback(); }, 3); - + var loadCargo = function(){ for(var i = 0; i < 10; i++){ c.push(i); } }; - + var drainCounter = 0; c.drain = function () { drainCounter++; - } + }; loadCargo(); setTimeout(loadCargo, 500); @@ -2853,7 +3142,7 @@ exports['cargo drain twice'] = function (test) { }; exports['memoize'] = function (test) { - test.expect(4); + test.expect(5); var call_order = []; var fn = function (arg1, arg2, callback) { @@ -2865,6 +3154,7 @@ exports['memoize'] = function (test) { var fn2 = async.memoize(fn); fn2(1, 2, function (err, result) { + test.ok(err === null, err + " passed instead of 'null'"); test.equal(result, 3); fn2(1, 2, function (err, result) { test.equal(result, 3); @@ -2940,7 +3230,7 @@ exports['unmemoize'] = function(test) { }); }); }); -} +}; exports['unmemoize a not memoized function'] = function(test) { test.expect(1); @@ -2955,7 +3245,7 @@ exports['unmemoize a not memoized function'] = function(test) { }); test.done(); -} +}; exports['memoize error'] = function (test) { test.expect(1); @@ -3024,22 +3314,22 @@ exports['falsy return values in series'] = function (test) { async.nextTick(function() { callback(null, false); }); - }; + } function taskUndefined(callback) { async.nextTick(function() { callback(null, undefined); }); - }; + } function taskEmpty(callback) { async.nextTick(function() { callback(null); }); - }; + } function taskNull(callback) { async.nextTick(function() { callback(null, null); }); - }; + } async.series( [taskFalse, taskUndefined, taskEmpty, taskNull], function(err, results) { @@ -3059,22 +3349,22 @@ exports['falsy return values in parallel'] = function (test) { async.nextTick(function() { callback(null, false); }); - }; + } function taskUndefined(callback) { async.nextTick(function() { callback(null, undefined); }); - }; + } function taskEmpty(callback) { async.nextTick(function() { callback(null); }); - }; + } function taskNull(callback) { async.nextTick(function() { callback(null, null); }); - }; + } async.parallel( [taskFalse, taskUndefined, taskEmpty, taskNull], function(err, results) { @@ -3102,12 +3392,12 @@ exports['queue events'] = function(test) { calls.push('saturated'); }; q.empty = function() { - test.ok(q.length() == 0, 'queue should be empty now'); + test.ok(q.length() === 0, 'queue should be empty now'); calls.push('empty'); }; q.drain = function() { test.ok( - q.length() == 0 && q.running() == 0, + q.length() === 0 && q.running() === 0, 'queue should be empty now and no more workers should be running' ); calls.push('drain'); @@ -3145,7 +3435,7 @@ exports['queue empty'] = function(test) { q.drain = function() { test.ok( - q.length() == 0 && q.running() == 0, + q.length() === 0 && q.running() === 0, 'queue should be empty now and no more workers should be running' ); calls.push('drain'); @@ -3157,11 +3447,30 @@ exports['queue empty'] = function(test) { q.push([]); }; +exports['queue saturated'] = function (test) { + var saturatedCalled = false; + var q = async.queue(function(task, cb) { + async.setImmediate(cb); + }, 2); + + q.saturated = function () { + saturatedCalled = true; + }; + q.drain = function () { + test.ok(saturatedCalled, "saturated not called"); + test.done(); + }; + + setTimeout(function () { + q.push(['foo', 'bar', 'baz', 'moo']); + }, 10); +}; + exports['queue started'] = function(test) { var calls = []; var q = async.queue(function(task, cb) {}); - + test.equal(q.started, false); q.push([]); test.equal(q.started, true); |