-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
498 lines (445 loc) · 16.1 KB
/
index.js
File metadata and controls
498 lines (445 loc) · 16.1 KB
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// Software License Agreement (ISC License)
//
// Copyright (c) 2021, Matthew Voss
//
// Permission to use, copy, modify, and/or distribute this software for
// any purpose with or without fee is hereby granted, provided that the
// above copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
var assign = require('qb-assign')
var jstr = require('qb-js-string')
// collects test argument and executes them later (on timeout) according to whether 'only'
// was called or not.
function TestRunner (test_module, test_fn, enrich_fns) {
this.inputs = [] // array of { args: [...], tk_props: { ... } }
this.only_called = false
this.running = false
this.test_module = test_module
this.test_fn = test_fn
this.enrich_fns = assign({}, enrich_fns)
}
TestRunner.prototype = {
constructor: TestRunner,
run: function () {
var self = this
setTimeout(function () {
self.running = true
self.inputs.forEach(function (input) {
self.test_fn.apply(self.test_module, enrich_test_arguments(input.args, self.enrich_fns, input.tk_props))
})
})
},
addTest: function (args, tk_props) {
if (this.running) {
throw Error('cannot add test - already running')
}
var input = { args: args, tk_props: tk_props }
if (tk_props.only) {
if (this.only_called) {
throw Error('there can only be one only test')
}
this.only_called = true
this.inputs = [input]
} else {
if (!this.only_called) {
this.inputs.push(input)
}
}
}
}
// given the arguments to tap.test(...), alter the first function to return an enriched 't' object as in:
//
// test('mytest', function(t) {...} )
//
// tk_props (optional) is added to the enriched function as '_tk_props' - for use by other functions (test modes)
//
function enrich_test_arguments (args, enrich_fns, tk_props) {
args = Array.prototype.slice.call(args)
var fi = args.findIndex(function (a) { return typeof (a) === 'function' })
args[fi] = enrich_t(args[fi], enrich_fns, tk_props)
return args
}
// enrich the test or 't' object by applying transforms in enrich_fns
// 'fn' is the user function that we will call with the new enriched 't': fn(t)
function enrich_t (fn, enrich_fns, tk_props) {
return function (torig) {
var tnew = Object.create(torig)
Object.keys(enrich_fns).forEach(function (n) { tnew[n] = enrich_fns[n](torig, tnew) })
tnew.tk_props = tk_props
fn(tnew)
}
}
// Return a string loosely based on JSON.stringify, but with single quotes and fewer escapes.
// (less precise, more readable)
//
// instead of:
//
// ok 1 - error: ("d--","/","a/b") -expect-> ("expect: parent \"a\" is not a directory")
//
// str() returns:
//
// ok 1 - error: ('d--','/','a/b') -expect-> ('expect: parent "a" is not a directory')
//
// str() converts undefined to null and doesn't handle cycles, so has room for improvement.
function str (v) {
if (v === undefined) return 'null'
return JSON.stringify(v, replacer).replace(/([^\\])"/g, "$1'").replace(/\\"/g, '"').replace(/\\\\/g, '\\')
}
function replacer (ignore, v) {
if(typeof v === 'function') {
v = (v.name && v.name + ' ()') || 'function ()'
}
return v
}
function parens (args) {
var ret = str(args)
return '(' + ret.substr(1, ret.length - 2) + ')'
}
function last (a) { return a[a.length - 1] }
function text_lines (s) {
var lines = s.split('\n')
for (var beg = 0; beg < lines.length && /^\s*$/.test(lines[beg]); beg++);
for (var end = lines.length - 1; end >= 0 && /^\s*$/.test(lines[end]); end--);
if (beg > end) { return [] }
var ind = countws(lines[beg])
for (var i = beg; i <= end; i++) {
var line = lines[i]
var ws = countws(line, ind)
lines[i] = ws === line.length ? '' : line.substring(Math.min(ws, ind))
}
return lines.slice(beg, end + 1)
}
function countws (s) {
for (var c = 0; c < s.length && s[c] === ' '; c++);
return c
}
function padl (s, l, c) { c = c || ' '; while (s.length < l) s = c + s; return s }
function padr (s, l, c) { c = c || ' '; while (s.length < l) s = s + c; return s }
function trunc (a) {
var i = a.length-1;
while (a[i] == null && i >= 0)
i--;
return Array.prototype.slice.call(a, 0, i+1)
}
function sum (a, prop_or_func) {
if (prop_or_func == null) {
return a.reduce(function (s, v) { return s + (v || 0) }, 0)
} else if (typeof prop_or_func === 'function') {
return a.reduce(function (s, v) { return s + prop_or_func(v) }, 0)
} else {
return a.reduce(function (s, v) { return s + (v[prop_or_func] || 0) }, 0)
}
}
function table (data) {
return require('test-table').create(data)
}
function table_assert (torig, tnew) {
return function (dataOrTable, fn, opt) {
fn && typeof fn === 'function' || err('invalid function argument: ' + fn)
opt = assign({}, {assert: 'same'}, opt)
var tbl = tnew.table(dataOrTable)
var tp = tnew.tk_props
if (tp.trows) {
tbl = tbl.trows.apply(tbl, tp.trows)
}
if (tp.print_mode) {
print_table(tnew, tbl, fn, opt, tp.print_mode)
} else {
assert_table(tnew, tbl, fn, opt)
}
}
}
function assert_table(tnew, tbl, fn, opt) {
if (opt.plan == null) {
opt.plan = (!tnew.planned_tests && opt.assert !== 'none') ? 1 : 0
} else {
opt.plan === 0 || !tnew.planned_tests || err('plan has already been set: ' + tnew.planned_tests)
}
if (opt.plan) { // non-zero
var plan_total
if (typeof opt.plan === 'string') {
plan_total = tnew.sum(tbl.vals(opt.plan))
} else {
plan_total = tbl.length * opt.plan
}
tnew.plan(plan_total) // sets planned_tests, which cannot be changed
}
tbl.rows.forEach(function (r) {
if (r._comments.length) {
r._comments.forEach(function (c) {
console.log(c)
})
}
var vals = r._vals()
var exp_val
if (opt.assert === 'none') {
if (opt.trunc) { vals = tnew.trunc(vals) }
fn.apply(null, vals)
} else {
vals = vals.slice()
exp_val = vals.pop()
if (opt.trunc) { vals = tnew.trunc(vals) }
if (opt.assert === 'throws') {
tnew.throws(function () { fn.apply(null, vals) }, exp_val, tnew.desc('', vals, exp_val.toString()))
} else {
tnew[opt.assert](fn.apply(null, vals), exp_val, tnew.desc('', vals, exp_val))
}
}
})
}
// same signature as table_assert, but pretty-print the table with results instead of running assertions
function print_table (tnew, tbl, fn, opt, print_mode) {
var out = opt.print_out || console.log
if (opt.assert === 'same' || opt.assert === 'equal') {
var last_header = tbl.header[tbl.header.length - 1]
// replace last column with results of output from first cols
tbl.rows.forEach(function (row) {
var vals = row._vals().slice()
vals.pop()
if (opt.trunc) { vals = tnew.trunc(vals) }
row[last_header] = fn.apply(null, vals)
})
} // else just format all cols (we can add special assert handling as needed)
var as_arrays = tbl.as_arrays({with_comments: true})
out('PRINT TABLE:')
out(jstr.table(as_arrays, {lang: print_mode}))
if (!opt.print_out) {
// if print out is set, then assume caller is doing the assertions.
tnew.ok(1, 'print table finished')
tnew.end()
}
}
function err (msg) { throw Error(msg) }
function type (v) {
var ret = Object.prototype.toString.call(v)
return ret.substring(8, ret.length - 1).toLowerCase()
}
function plan (torig, tnew) {
return function (n) {
tnew.planned_tests = n // mark tests as planned (see tableAssert)
return torig.plan(n)
}
}
function countstr (src, v) {
type(v) === 'string' || err('value should be a string: ' + type(v))
v.length > 0 || err('cannot count zero-length string')
var c = 0, i = 0
if (v.length === 1) {
var len = src.length
for (i = 0; i < len; i++) { if (src[i] === v) c++ }
} else {
for (i = src.indexOf(v); i !== -1; i = src.indexOf(v, i + 1)) { c++ }
}
return c
}
function countbuf (src, v) {
switch (type(v)) {
case 'string':
v.length === 1 || err('long strings not supported')
v = v.charCodeAt(0)
break
case 'number':
break
default:
throw Error('type not handled: ' + type(v))
}
v === (v & 0xFF) || err('value for uint8array should be a byte (0-255)')
var c = 0, len = src.length
for (var i = 0; i < len; i++) { if (src[i] === v) c++ }
return c
}
function count (src, v) {
switch (type(src)) {
case 'uint8array':
return countbuf(src, v)
case 'string':
return countstr(src, v)
case 'array':
for (var i = 0, c = 0; i < src.length; i++) { if (src[i] === v) c++ }
return c
default:
throw Error('type not handled: ' + type(src))
}
}
// inverse match (see readme)
function imatch (s, re, opt) {
opt = Object.assign({}, {empties: 'ignore', return: 'strings', no_match: 'string'}, opt)
var prep_result = function (res) {
if (opt.empties !== 'include') {
res = res.filter(function (tpl) { return tpl[1] !== 0 })
}
return opt.return === 'tuples' ? res : res.map(function (tpl) { return s.substr(tpl[0], tpl[1]) })
}
var m = re.exec(s)
if (!m) {
switch (opt.no_match) {
case 'null' : return null
case 'string' : return prep_result([[0, s.length]])
case 'throw' : // fall-through
default : throw Error(re.toString() + ' does not match string ' + s)
}
}
var ret = []
var off = 0
do {
var len = m.index - off
ret.push([off, len])
off = m.index + m[0].length
} while (re.lastIndex && (m = re.exec(s)) !== null)
ret.push([off, s.length - off])
return prep_result(ret)
}
function ireplace (s, re, fn_or_string, opt) {
var fn = typeof fn_or_string === 'function' ? fn_or_string : function () { return fn_or_string }
opt = assign({}, opt)
opt.return = 'tuples' // other imatch options 'empty' and 'no_match' are client-controlled.
var m = imatch(s, re, opt)
if (m === null) {
return null // opt.empty was 'null'
}
var ret = []
var off = 0
m.forEach(function (tpl) {
var toff = tpl[0], tlen = tpl[1]
ret.push(s.substring(off, toff)) // matched portion (added intact)
ret.push(fn(s.substr(toff, tlen), toff, s)) // unmatched portion (transform)
off = toff + tlen
})
ret.push(s.substring(off, s.length)) // remaining matched portion
return ret.join('')
}
function hector (names) {
var args = []
var max_num_args = 0
var ret = function () {
args.push(Array.prototype.slice.call(arguments))
max_num_args = arguments.length > max_num_args ? arguments.length : max_num_args
}
ret.args = args // make args a simple/visible property
ret.arg = function arg (which) {
var i = which
if (typeof i === 'string') {
i = names ? names.indexOf(which) : -1 // no names will return array of undefined
}
return args.map(function (list) { return list[i] })
}
return ret
}
// return a one-line string describing expected input and output of the form:
//
// lbl: [input_a, input_b..] -expect-> output
//
function desc (lbl, inp, out) {
return lbl + ': ' + parens(inp) + ' -expect-> ' + parens([out])
}
function tkprop (torig, tnew) {
return function () {
var k = arguments[0]
switch (arguments.length) {
case 0: return
case 1: return tnew.tk_props[k]
default:
var v = arguments[1]
if (v == null) {
delete tnew.tk_props[k]
} else {
tnew.tk_props[k] = v
}
return
}
}
}
// Heap's Algorithm for generating all permutations of array 'a'
function permut (a) { var p = []; _heaps(a.slice(), a.length, p); return p }
function swap(a, i, j) { var t = a[i]; a[i] = a[j]; a[j] = t }
function _heaps(a, n, p) {
if (n === 1) {
p.push(a.slice())
} else {
for (var i = 0; i < n; i++) {
_heaps(a, n - 1, p)
swap(a, n % 2 ? 0 : i, n - 1)
}
}
}
// Creation functions are passed the original test object and the new test
// object so they may invoke new or prior-defined functions (delegate).
var DEFAULT_FUNCTIONS = {
count: function () { return count },
desc: function () { return desc },
hector: function () { return hector },
permut: function () { return permut },
imatch: function () { return imatch },
ireplace: function () { return ireplace },
last: function () { return last },
lines: function () { return text_lines },
padl: function () { return padl },
padr: function () { return padr },
plan: function (torig, tnew) { return plan(torig, tnew) },
str: function () { return str },
sum: function () { return sum },
table: function () { return table },
tableAssert: function (torig, tnew) { return table_assert(torig, tnew) }, // backward-compatibility
table_assert: function (torig, tnew) { return table_assert(torig, tnew) },
tkprop: function (torig, tnew) { return tkprop(torig, tnew) },
trunc: function () { return trunc },
type: function () { return type },
utf8: function () { return require('qb-utf8-ez').buffer },
utf8_to_str: function () { return require('qb-utf8-ez').string }
}
function testfn (name_or_fn, custom_fns, opt) {
opt = opt || {}
var test_module = null
var test_orig = name_or_fn
if (typeof name_or_fn === 'string') {
try {
test_module = require(name_or_fn)
test_orig = test_module.test
} catch(e) {
var suggest = (typeof custom_fns === 'function') ? ' (It looks like the call to tape or tap was left out as in "require(\'test-kit\').tape()")' : ''
err('could not load ' + name_or_fn + suggest + ': ' + e)
}
}
// it isn't clear that passing in a test function is being used anywhere - consider deprecation and removal
typeof test_orig === 'function' || err(name_or_fn + ' is not a function')
var enrich_fns = assign({}, opt.custom_only ? {} : DEFAULT_FUNCTIONS, custom_fns)
var ret
var runner = new TestRunner(test_module, test_orig, enrich_fns)
ret = function () { runner.addTest(arguments, {}) }
ret.only = function () { runner.addTest(arguments, {only: true}) }
ret.print = function () { runner.addTest(arguments, {only: true, print_mode: 'js'}) }
ret.java = function () { runner.addTest(arguments, {only: true, print_mode: 'java' }) }
ret.only1 = function () { runner.addTest(arguments, {only: true, trows: [0,1] })}
runner.run()
ret.engine = test_orig.only && test_orig.onFinish ? 'tape' : 'tap' // just a guess by what is likely
Object.keys(test_orig).forEach(function (k) {
if (!ret[k]) {
var orig = test_orig[k]
if (typeof orig === 'function') {
ret[k] = function () { return orig.apply(test_orig, arguments) } // call function with original context
} else {
ret[k] = orig
}
}
})
// ret.onFinish = test_orig.onFinish // only available in tape
return ret
}
// property/function transforms applied to the test object passed into each test:
//
// test( 'my test', function(t) {...} ) // applied to the 't' object
//
// return a simple description of a function test: inputs -> outputs
testfn.DEFAULT_FUNCTIONS = DEFAULT_FUNCTIONS
module.exports = testfn
// these convenience functions use dynamic 'require()' that allows test-kit to NOT depend on both tap and tape -
// so required dependencies are kept light.
module.exports.tap = function (custom_fns, opt) { return testfn('tap', custom_fns, opt) }
module.exports.tape = function (custom_fns, opt) { return testfn('tape', custom_fns, opt) }