1 |
|
2 |
|
3 | !function(exports) {
|
4 | var doneTick, lastSuite, lastCase, started, ended
|
5 | , _global = exports.window || global
|
6 | , Fn = exports.Fn || require("../lib/fn").Fn
|
7 | , assert = exports.assert || require("./assert")
|
8 | , nativeTimeout = setTimeout
|
9 | , nativeClearTimeout = clearTimeout
|
10 | , nativeDate = Date
|
11 | , empty = {}
|
12 | , hasOwn = empty.hasOwnProperty
|
13 |
|
14 | , fakeNow
|
15 | , timers = []
|
16 | , timerId = 0
|
17 | , fakeTimers = {
|
18 | setTimeout: fakeTimeout.bind(null, false),
|
19 | setInterval: fakeTimeout.bind(null, true),
|
20 | clearTimeout: fakeClear,
|
21 | clearInterval: fakeClear,
|
22 | Date: fakeDate
|
23 | }
|
24 |
|
25 | , color = (process.stdout || empty).isTTY && process.argv.indexOf("--no-color") == -1
|
26 | , only = process.argv.slice(2)
|
27 | , totalCases = 0
|
28 | , failedCases = 0
|
29 | , skipCases = 0
|
30 | , lastAssert = 0
|
31 | , skipAssert = 0
|
32 | , passedAsserts = 0
|
33 | , bold = color ? "\x1b[1m" : ""
|
34 | , italic = color ? "\x1b[3m" : ""
|
35 | , strike = color ? "\x1b[9m" : ""
|
36 | , underline = color ? "\x1b[4m" : ""
|
37 | , red = color ? "\x1b[31m" : ""
|
38 | , green = color ? "\x1b[32m" : ""
|
39 | , yellow = color ? "\x1b[33m" : ""
|
40 | , reset = color ? "\x1b[0m" : ""
|
41 |
|
42 |
|
43 | exports.defineAssert = defineAssert
|
44 | exports.describe = describe
|
45 | exports.test = function(name, next, opts) {
|
46 | return (lastSuite || describe()).test(name, next)
|
47 | }
|
48 | exports.it = function(name, next, opts) {
|
49 | return exports.test("it " + name, next, opts)
|
50 | }
|
51 |
|
52 |
|
53 | function TestSuite(name) {
|
54 | lastSuite = this
|
55 | checkEnd(lastAssert)
|
56 | if (!started) {
|
57 | started = nativeDate.now()
|
58 | if (!only.length) {
|
59 | print("TAP version 13")
|
60 | }
|
61 | }
|
62 | if (lastCase && !lastCase.ended) {
|
63 | lastCase.end()
|
64 | }
|
65 | if (!only.length) {
|
66 | print("# " + (name || "{unnamed test suite}"))
|
67 | }
|
68 | }
|
69 |
|
70 | TestSuite.prototype = {
|
71 | wait: Fn.hold,
|
72 | describe: describe,
|
73 | it: function(name, next, opts) {
|
74 | return this.test("it " + name, next, opts)
|
75 | },
|
76 | test: function(name, next, opts) {
|
77 | if (lastCase && !lastCase.ended) {
|
78 | lastCase.end()
|
79 | }
|
80 | if (typeof name === "function") {
|
81 | next = name
|
82 | name = ""
|
83 | }
|
84 | if (typeof next !== "function") {
|
85 | opts = next
|
86 | next = null
|
87 | }
|
88 | var testSuite = this
|
89 | , testCase = lastCase = new TestCase(name, opts)
|
90 | checkEnd()
|
91 |
|
92 | ;["describe", "it", "test"].forEach(function(name) {
|
93 | testCase[name] = function() {
|
94 | return testSuite[name].apply(testSuite, arguments)
|
95 | }
|
96 | })
|
97 |
|
98 | if (next && !testCase.opts.skip) {
|
99 | nativeClearTimeout(doneTick)
|
100 | testCase.setTimeout()
|
101 | testCase.resume = testSuite.wait()
|
102 | next(
|
103 | testCase,
|
104 | (testCase.mock = next.length > 1 && new Mock)
|
105 | )
|
106 | return testSuite
|
107 | }
|
108 |
|
109 | return testCase
|
110 | },
|
111 | _it: This,
|
112 | _test: This
|
113 | }
|
114 |
|
115 | function TestCase(name, opts) {
|
116 | var testCase = this
|
117 | , opts = testCase.opts = opts || {}
|
118 | , id = ++totalCases
|
119 | testCase.name = id + " - " + (name || "{unnamed test case}")
|
120 | testCase.failed = []
|
121 | testCase.passedAsserts = 0
|
122 | testCase.totalAsserts = 0
|
123 |
|
124 | if (only.length && only.indexOf("" + id) === -1) {
|
125 | opts.skip = "command line"
|
126 | }
|
127 |
|
128 | return testCase
|
129 | }
|
130 |
|
131 | TestCase.prototype = {
|
132 | plan: function(num) {
|
133 | this.planned = num
|
134 | return this
|
135 | },
|
136 | setTimeout: function(ms) {
|
137 | var testCase = this
|
138 | nativeClearTimeout(testCase.timeout)
|
139 | testCase.timeout = nativeTimeout(function() {
|
140 | throw Error("Timeout on running '" + testCase.name + "'")
|
141 | }, ms || 5000)
|
142 | return testCase
|
143 | },
|
144 | end: function() {
|
145 | var testCase = this
|
146 | , name = testCase.name
|
147 | , n = "\n "
|
148 |
|
149 | if (testCase.ended) {
|
150 | failedCases++
|
151 | throw Error("'" + name + "' ended multiple times")
|
152 | }
|
153 |
|
154 | testCase.ended = nativeDate.now()
|
155 | name += " [" + testCase.passedAsserts + "/" + testCase.totalAsserts + "]"
|
156 |
|
157 | if (testCase.opts.skip) {
|
158 | skipCases++
|
159 | if (only.length === 0) {
|
160 | print("ok " + name + " # skip - " + testCase.opts.skip)
|
161 | }
|
162 | return
|
163 | }
|
164 |
|
165 | if (testCase.planned != void 0 && testCase.planned !== testCase.totalAsserts) {
|
166 | testCase.failed.push("Planned " + testCase.planned + " actual " + testCase.totalAsserts)
|
167 | }
|
168 |
|
169 | if (testCase.failed.length) {
|
170 | failedCases++
|
171 | print("not ok " + name + n + "---\n" + testCase.failed.join("\n").replace(/^/gm, " ") + n + "...")
|
172 | } else {
|
173 | print("ok " + name)
|
174 | }
|
175 | if (testCase.timeout) {
|
176 | nativeClearTimeout(testCase.timeout)
|
177 | testCase.timeout = null
|
178 | if (testCase.mock) {
|
179 | testCase.mock.restore()
|
180 | }
|
181 | testCase.resume()
|
182 | checkEnd()
|
183 | }
|
184 | }
|
185 | }
|
186 |
|
187 | Object.keys(assert).forEach(defineAssert)
|
188 |
|
189 | chainable(TestSuite, TestCase)
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | if (_global.setImmediate) {
|
195 | fakeTimers.setImmediate = fakeNextTick
|
196 | fakeTimers.clearImmediate = fakeClear
|
197 | }
|
198 | function fakeDate(year, month, date, hr, min, sec, ms) {
|
199 | return (
|
200 | arguments.length > 1 ?
|
201 | new nativeDate(year|0, month|0, date||1, hr|0, min|0, sec|0, ms|0) :
|
202 | new nativeDate(year || Math.floor(fakeNow))
|
203 | )
|
204 | }
|
205 | fakeDate.now = function() {
|
206 | return Math.floor(fakeNow)
|
207 | }
|
208 | fakeDate.parse = nativeDate.parse
|
209 |
|
210 | function fakeHrtime(time) {
|
211 | var diff = Array.isArray(time) ? fakeNow - (time[0] * 1e3 + time[1] / 1e6) : fakeNow
|
212 | return [Math.floor(diff / 1000), Math.round((diff % 1e3) * 1e3) * 1e3]
|
213 | }
|
214 |
|
215 | function fakeTimeout(repeat, fn, ms) {
|
216 | if (typeof repeat !== "object") {
|
217 | repeat = {
|
218 | id: ++timerId,
|
219 | repeat: repeat,
|
220 | fn: fn,
|
221 | args: timers.slice.call(arguments, 3),
|
222 | at: fakeNow + ms,
|
223 | ms: ms
|
224 | }
|
225 | }
|
226 | for (var i = timers.length; i--; ) {
|
227 | if (timers[i].at <= repeat.at) break
|
228 | }
|
229 | timers.splice(i + 1, 0, repeat)
|
230 | return repeat.id
|
231 | }
|
232 |
|
233 | function fakeNextTick(fn) {
|
234 | fakeTimeout({
|
235 | id: ++timerId,
|
236 | fn: fn,
|
237 | args: timers.slice.call(arguments, 1),
|
238 | at: fakeNow - 1
|
239 | })
|
240 | }
|
241 |
|
242 | function fakeClear(id) {
|
243 | for (var i = timers.length; i--; ) {
|
244 | if (timers[i].id === id) {
|
245 | timers.splice(i, 1)
|
246 | break
|
247 | }
|
248 | }
|
249 | }
|
250 |
|
251 | function Mock() {
|
252 | var mock = this
|
253 | mock.replaced = []
|
254 | }
|
255 | Mock.prototype = {
|
256 | fn: function(origin) {
|
257 | spy.called = 0
|
258 | spy.calls = []
|
259 | return spy
|
260 | function spy() {
|
261 | var fn, result
|
262 | , args = spy.calls.slice.call(arguments)
|
263 | if (typeof origin === "function") {
|
264 | result = origin.apply(this, arguments)
|
265 | } else if (Array.isArray(origin)) {
|
266 | result = origin[spy.called % origin.length]
|
267 | } else if (origin && origin.constructor === Object) {
|
268 | result = origin[JSON.stringify(args).slice(1,-1)]
|
269 | fn = typeof origin.fn === "function" ? origin.fn : typeof result === "function" ? result : null
|
270 | if (fn) {
|
271 | result = fn.call(this, result)
|
272 | }
|
273 | }
|
274 | spy.called++
|
275 | spy.calls.push({
|
276 | scope: this,
|
277 | args: args,
|
278 | result: result
|
279 | })
|
280 | return result
|
281 | }
|
282 | },
|
283 | map: function(obj, stubs, justStubs) {
|
284 | var key
|
285 | , mock = this
|
286 | , obj2 = justStubs ? stubs : obj
|
287 | for (key in obj2) {
|
288 | mock.spy(obj, key, stubs && stubs[key])
|
289 | }
|
290 | if (obj.prototype) {
|
291 | mock.map(obj.prototype, stubs)
|
292 | }
|
293 | },
|
294 | replace: function(obj, name, fn) {
|
295 | var mock = this
|
296 | if (typeof obj[name] === "function") {
|
297 | mock.replaced.push(obj, name, hasOwn.call(obj, name) && obj[name])
|
298 | obj[name] = fn
|
299 | }
|
300 | },
|
301 | spy: function(obj, name, stub) {
|
302 | var mock = this
|
303 | mock.replace(obj, name, mock.fn(stub || obj[name]))
|
304 | },
|
305 | time: function(newTime) {
|
306 | var key
|
307 | , mock = this
|
308 | if (!mock.timeFreeze) {
|
309 | mock.timeFreeze = fakeNow = nativeDate.now()
|
310 | for (key in fakeTimers) {
|
311 | mock.replace(_global, key, fakeTimers[key])
|
312 | }
|
313 | if (process.nextTick) {
|
314 | mock.replace(process, "nextTick", fakeNextTick)
|
315 | mock.replace(process, "hrtime", fakeHrtime)
|
316 | }
|
317 | }
|
318 | if (newTime) {
|
319 | fakeNow = typeof newTime === "string" ? nativeDate.parse(newTime) : newTime
|
320 | mock.tick(0)
|
321 | }
|
322 | },
|
323 | tick: function(amount, noRepeat) {
|
324 | if (typeof amount === "number") {
|
325 | fakeNow += amount
|
326 | } else if (timers[0]) {
|
327 | fakeNow = timers[0].at
|
328 | }
|
329 |
|
330 | for (var t; t = timers[0]; ) {
|
331 | if (t.at <= fakeNow) {
|
332 | timers.shift()
|
333 | if (typeof t.fn === "function") t.fn.apply(null, t.args)
|
334 | if (!noRepeat && t.repeat) {
|
335 | t.at += t.ms
|
336 | fakeTimeout(t)
|
337 | }
|
338 | } else {
|
339 | break
|
340 | }
|
341 | }
|
342 | },
|
343 | restore: function() {
|
344 | var mock = this
|
345 | , replaced = mock.replaced
|
346 | , i = replaced.length
|
347 | for (; --i > 0; i -= 2) {
|
348 | if (replaced[i]) {
|
349 | replaced[i - 2][replaced[i - 1]] = replaced[i]
|
350 | } else {
|
351 | delete replaced[i - 2][replaced[i - 1]]
|
352 | }
|
353 | }
|
354 | if (timers.length) {
|
355 | mock.tick(Infinity, true)
|
356 | }
|
357 | }
|
358 | }
|
359 |
|
360 | function print(str) {
|
361 | console.log(str + reset)
|
362 | }
|
363 |
|
364 | function describe(name) {
|
365 | return lastSuite && lastSuite !== this ? lastSuite.describe(name) : new TestSuite(name)
|
366 | }
|
367 |
|
368 | function checkEnd() {
|
369 | var curSuite = lastSuite
|
370 | , curCase = lastCase
|
371 | , curAssert = lastAssert
|
372 |
|
373 | nativeClearTimeout(doneTick)
|
374 | doneTick = setTimeout(function() {
|
375 | if (curAssert === lastAssert && curCase === lastCase && lastCase && !lastCase.timeout && curSuite == lastSuite) {
|
376 | if (!lastCase.ended) {
|
377 | lastCase.end()
|
378 | }
|
379 | end()
|
380 | }
|
381 | }, 1)
|
382 | }
|
383 |
|
384 | function end() {
|
385 | if (ended) {
|
386 | throw Error("ended in multiple times")
|
387 | }
|
388 | nativeClearTimeout(doneTick)
|
389 | ended = nativeDate.now()
|
390 |
|
391 | if (!only.length) {
|
392 | print("1.." + totalCases)
|
393 |
|
394 | if (skipAssert) {
|
395 | print("# " + yellow + bold + "skip " + skipCases + "/" + skipAssert)
|
396 | }
|
397 |
|
398 | print("#" + (failedCases ? "" : green + bold) + " pass " + (totalCases - failedCases)
|
399 | + "/" + totalCases
|
400 | + " [" + passedAsserts + "/" + lastAssert + "]"
|
401 | + " in " + (ended - started) + " ms")
|
402 |
|
403 | failedCases && print("#" + red + bold + " fail " + failedCases
|
404 | + " [" + (lastAssert - passedAsserts) + "]")
|
405 | }
|
406 |
|
407 | if (process.exit) {
|
408 | process.exit(failedCases ? 1 : 0)
|
409 | }
|
410 | |
411 |
|
412 |
|
413 |
|
414 |
|
415 | }
|
416 |
|
417 | function defineAssert(key, fn, _skip) {
|
418 | if (!assert[key]) {
|
419 | assert[key] = fn
|
420 | }
|
421 | TestSuite.prototype["_" + key] = TestCase.prototype["_" + key] = skip
|
422 | TestSuite.prototype[key] = _skip === true ? skip : function() {
|
423 | var testCase = this.test("", null)
|
424 | return testCase[key].apply(testCase, arguments)
|
425 | },
|
426 | TestCase.prototype[key] = _skip === true ? skip : assertWrapper
|
427 | function assertWrapper(a, b, c) {
|
428 | var testCase = this
|
429 | if (testCase.opts.skip) {
|
430 | return skip.call(testCase)
|
431 | }
|
432 | lastAssert++
|
433 | if (!testCase.timeout) checkEnd()
|
434 | testCase.totalAsserts++
|
435 | try {
|
436 | assert[key].call(assert, a, b, c, assertWrapper)
|
437 | passedAsserts++
|
438 | testCase.passedAsserts++
|
439 | } catch(e) {
|
440 | testCase.failed.push(testCase.opts.noStack ? e.message : e.stack)
|
441 | }
|
442 | if (testCase.planned != null && testCase.planned <= testCase.totalAsserts) {
|
443 | testCase.end()
|
444 | }
|
445 | return testCase
|
446 | }
|
447 | return this
|
448 | }
|
449 |
|
450 | function chainable() {
|
451 | var a
|
452 | , arr = []
|
453 | , j, i = 0
|
454 | for (; a = arguments[i++]; ) {
|
455 | arr.push.apply(arr, Object.keys(a.prototype))
|
456 | }
|
457 | for (i = 0; a = arguments[i++]; ) {
|
458 | for (j = arr.length; j--; ) {
|
459 | if (!a.prototype[arr[j]]) {
|
460 | a.prototype[arr[j]] = This
|
461 | }
|
462 | }
|
463 | }
|
464 | }
|
465 |
|
466 | function skip() {
|
467 | skipAssert++
|
468 | return this
|
469 | }
|
470 |
|
471 | function This() {
|
472 | return this
|
473 | }
|
474 |
|
475 | describe.diff = diff
|
476 | describe.colorDiff = colorDiff
|
477 |
|
478 | function colorDiff(a, b) {
|
479 | var res = diff(a, b)
|
480 | console.log(
|
481 | a.slice(0, res[0]) +
|
482 | bold + red + strike + a.slice(res[0], res[0] + res[1]) +
|
483 | green + b.slice(res[0], res[0]+res[2]) +
|
484 | reset + a.slice(res[0] + res[1])
|
485 | )
|
486 | }
|
487 |
|
488 | function diff(a, b, re) {
|
489 | var c = 0, d = a.length, e = b.length
|
490 | for (; a.charAt(c) && a.charAt(c) == b.charAt(c); c++);
|
491 | for (; d > c && e > c && a.charAt(d - 1) == b.charAt(e - 1); d--) e--;
|
492 | return [c, d - c, e - c]
|
493 | }
|
494 | }(this)
|
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 |
|