forked from practicalmeteor/meteor-munit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
async_multi.js
executable file
·177 lines (156 loc) · 4.73 KB
/
async_multi.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// This depends on tinytest, so it's a little weird to put it in
// test-helpers, but it'll do for now.
// Provides the testAsyncMulti helper, which creates an async test
// (using Tinytest.addAsync) that tracks parallel and sequential
// asynchronous calls. Specifically, the two features it provides
// are:
// 1) Executing an array of functions sequentially when those functions
// contain async calls.
// 2) Keeping track of when callbacks are outstanding, via "expect".
//
// To use, pass an array of functions that take arguments (test, expect).
// (There is no onComplete callback; completion is determined automatically.)
// Expect takes a callback closure and wraps it, returning a new callback closure,
// and making a note that there is a callback oustanding. Pass this returned closure
// to async functions as the callback, and the machinery in the wrapper will
// record the fact that the callback has been called.
//
// A second form of expect takes data arguments to test for.
// Essentially, expect("foo", "bar") is equivalent to:
// expect(function(arg1, arg2) { test.equal([arg1, arg2], ["foo", "bar"]); }).
//
// You cannot "nest" expect or call it from a callback! Even if you have a chain
// of callbacks, you need to call expect at the "top level" (synchronously)
// but the callback you wrap has to be the last/innermost one. This sometimes
// leads to some code contortions and should probably be fixed.
// Example: (at top level of test file)
//
// testAsyncMulti("test name", [
// function(test, expect) {
// ... tests here
// Meteor.defer(expect(function() {
// ... tests here
// }));
//
// call_something_async('foo', 'bar', expect('baz')); // implicit callback
//
// },
// function(test, expect) {
// ... more tests
// }
// ]);
var ExpectationManager = function (test, onComplete) {
var self = this;
self.test = test;
self.onComplete = onComplete;
self.closed = false;
self.dead = false;
self.outstanding = 0;
};
_.extend(ExpectationManager.prototype, {
expect: function (/* arguments */) {
var self = this;
if (typeof arguments[0] === "function")
var expected = arguments[0];
else
var expected = _.toArray(arguments);
if (self.closed)
throw new Error("Too late to add more expectations to the test");
self.outstanding++;
return function (/* arguments */) {
if (self.dead)
return;
if (typeof expected === "function") {
try {
expected.apply({}, arguments);
} catch (e) {
if (self.cancel())
self.test.exception(e);
}
} else {
self.test.equal(_.toArray(arguments), expected);
}
self.outstanding--;
self._check_complete();
};
},
done: function () {
var self = this;
self.closed = true;
self._check_complete();
},
cancel: function () {
var self = this;
if (! self.dead) {
self.dead = true;
return true;
}
return false;
},
_check_complete: function () {
var self = this;
if (!self.outstanding && self.closed && !self.dead) {
self.dead = true;
self.onComplete();
}
}
});
lvTestAsyncMulti = function (name,timeout,funcs) {
// XXX Tests on remote browsers are _slow_. We need a better solution.
// Backward compatibility
if( Array.isArray(timeout) ){
funcs = timeout;
timeout = 30000; // 30 seconds
}
Tinytest.addAsync(name, function (test, onComplete) {
var remaining = _.clone(funcs);
var context = {};
var runNext = function () {
var func = remaining.shift();
if (!func)
onComplete();
else {
var em = new ExpectationManager(test, function () {
Meteor.clearTimeout(timer);
runNext();
});
var timer = Meteor.setTimeout(function () {
if (em.cancel()) {
test.fail({type: "timeout", message: "Async batch timed out"});
onComplete();
}
return;
}, timeout);
try {
func.apply(context, [test, _.bind(em.expect, em)]);
} catch (exception) {
if (em.cancel())
test.exception(exception);
Meteor.clearTimeout(timer);
// Because we called test.exception, we're not to call onComplete.
return;
}
em.done();
}
};
runNext();
});
};
pollUntil = function (expect, f, timeout, step, noFail) {
noFail = noFail || false;
step = step || 100;
var expectation = expect(true);
var start = (new Date()).valueOf();
var helper = function () {
if (f()) {
expectation(true);
return;
}
if (start + timeout < (new Date()).valueOf()) {
expectation(noFail);
return;
}
Meteor.setTimeout(helper, step);
};
helper();
};