Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Avatar for KuCalc : devops.
Download
50640 views
1
###############################################################################
2
#
3
# CoCalc: Collaborative web-based calculation
4
# Copyright (C) 2017, Sagemath Inc.
5
# AGPLv3
6
#
7
###############################################################################
8
9
require('coffee-cache')
10
11
misc = require('../misc')
12
underscore = require('underscore')
13
14
# ATTN: the order of these require statements is important,
15
# such that should & sinon work well together
16
assert = require('assert')
17
expect = require('expect')
18
sinon = require('sinon')
19
should = require('should')
20
require('should-sinon')
21
22
# introduction to the testing frameworks
23
24
# documentation
25
# mocha: http://mochajs.org/
26
# should.js: http://shouldjs.github.io/
27
# sinon.js: http://sinonjs.org/docs/
28
# should-sinon: https://github.com/shouldjs/sinon/blob/master/test.js
29
# general notes: http://www.kenpowers.net/blog/testing-in-browsers-and-node/
30
31
describe "should.js (http://shouldjs.github.io/)", ->
32
it "tests that should.js throws errors", ->
33
# ATTN don't forget the () after true and similar!
34
expect(-> (false).should.be.true()).toThrow()
35
# because otherwise no error:
36
expect(-> (false).should.be.true).toNotThrow()
37
38
describe "expect (https://github.com/mjackson/expect)", ->
39
# ATTN when you want to check for an error, wrap it in (-> call())...
40
(-> expect(false).toBe(true)).should.throw()
41
42
describe "sinon", ->
43
it "is working", ->
44
object = method: () -> {}
45
spy = sinon.spy(object, "method");
46
spy.withArgs(42);
47
spy.withArgs(1);
48
49
object.method(1);
50
object.method(42);
51
object.method(1);
52
53
assert(spy.withArgs(42).calledOnce)
54
assert(spy.withArgs(1).calledTwice)
55
56
it "unit test", ->
57
callback = sinon.spy();
58
expect(callback.callCount).toEqual 0
59
callback.should.have.callCount 0
60
61
callback();
62
expect(callback.calledOnce).toBe true
63
callback.should.be.calledOnce()
64
callback("xyz");
65
expect(callback.callCount).toEqual 2
66
callback.should.have.callCount 2
67
expect(callback.getCall(1).args[0]).toEqual "xyz"
68
(-> expect(callback.getCall(1).args[0]).toEqual "1").should.throw()
69
callback.getCall(1).args[0].should.eql "xyz"
70
71
describe "sinon's stubs", ->
72
# also see the test for console.debug /w the console.log stub below
73
it "are working for withArgs", ->
74
func = sinon.stub()
75
func.withArgs(42).returns(1)
76
func.throws()
77
78
expect(func(42)).toEqual(1)
79
expect(func).toThrow(Error)
80
81
it "and for onCall", ->
82
func = sinon.stub()
83
#func.onCall(0).throws() # what the heck is this testing!?
84
func.onCall(1).returns(42)
85
86
#expect(func()).toThrow(Error) # i don't even understand this test
87
#expect(func()).toEqual(42);
88
89
# start testing misc.coffee
90
91
describe 'startswith', ->
92
startswith = misc.startswith
93
it 'checks that "foobar" starts with foo', ->
94
startswith("foobar",'foo').should.be.true()
95
it 'checks that "foobar" does not start with bar', ->
96
startswith("foobar",'bar').should.be.false()
97
it "works well with too long search strings", ->
98
startswith("bar", "barfoo").should.be.false()
99
it 'checks that "bar" starts in any of the given strings (a list)', ->
100
startswith("barbatz", ["aa", "ab", "ba", "bb"]).should.be.true()
101
it 'checks that "catz" does not start with any of the given strings (a list)', ->
102
startswith("catz", ["aa", "ab", "ba", "bb"]).should.be.false()
103
104
describe "endswith", ->
105
endswith = misc.endswith
106
it 'checks that "foobar" ends with "bar"', ->
107
endswith("foobar", "bar").should.be.true()
108
it 'checks that "foobar" does not end with "foo"', ->
109
endswith("foobar", "foo").should.be.false()
110
it "works well with too long search strings", ->
111
endswith("foo", "foobar").should.be.false()
112
it "doesn't work with arrays", ->
113
(-> endswith("foobar", ["aa", "ab"])).should.not.throw()
114
it "is false if either argument is undefined", ->
115
endswith(undefined, '...').should.be.false()
116
endswith('...', undefined).should.be.false()
117
endswith(undefined, undefined).should.be.false()
118
119
describe 'random_choice and random_choice_from_obj', ->
120
rc = misc.random_choice
121
rcfo = misc.random_choice_from_obj
122
it 'checks that a randomly chosen element is in the given list', ->
123
for i in [1..10]
124
l = ["a", 5, 9, {"ohm": 123}, ["batz", "bar"]]
125
l.should.containEql rc(l)
126
it "random_choice properly selects *all* available elements", ->
127
l = [3, 1, "x", "uvw", 1, [1,2,3]]
128
while l.length > 0
129
l.pop(rc(l))
130
l.should.have.length 0
131
it 'checks that random choice works with only one element', ->
132
rc([123]).should.be.eql 123
133
it 'checks that random choice with no elements is also fine', ->
134
should(rc([])).be.undefined() # i.e. undefined or something like that
135
it 'checks that a randomly chosen key/value pair from an object exists', ->
136
o = {abc : [1, 2, 3], cdf : {a: 1, b:2}}
137
[["abc", [1, 2, 3]], ["cdf" , {a: 1, b:2}]].should.containEql rcfo(o)
138
139
describe 'the Python flavoured randint function', ->
140
randint = misc.randint
141
it 'includes both interval bounds', ->
142
lb = -4; ub = 7
143
xmin = xmax = 0
144
for i in [1..1000]
145
x = randint(lb, ub)
146
x.should.be.within(lb, ub)
147
xmin = Math.min(xmin, x)
148
xmax = Math.max(xmax, x)
149
break if xmin == lb and xmax == ub
150
xmin.should.be.exactly lb
151
xmax.should.be.exactly ub
152
it 'behaves well for tight intervals', ->
153
randint(91, 91).should.be.exactly 91
154
it 'behaves badly with flipped intervals bounds', ->
155
# note about using should:
156
# this -> function invocation is vital to capture the error
157
(-> randint(5, 2)).should.throw /lower is larger than upper/
158
159
describe 'the Python flavoured split function', ->
160
split = misc.split
161
it 'splits correctly on whitespace', ->
162
s = "this is a sentence"
163
split(s).should.eql ["this", "is", "a", "sentence"]
164
it "splits also on linebreaks and special characters", ->
165
s2 = """we'll have
166
a lot (of)
167
fun\nwith sp|äci|al cħæ¶ä¢ŧ€rß"""
168
split(s2).should.eql ["we'll", "have", "a", "lot", "(of)",
169
"fun", "with", "sp|äci|al", "cħæ¶ä¢ŧ€rß"]
170
it "handles empty and no matches correctly", ->
171
split("").should.be.eql []
172
split("\t").should.be.eql []
173
174
describe 'search_split is like split, but quoted terms are grouped together', ->
175
ss = misc.search_split
176
it "correctly with special characters", ->
177
s1 = """Let's check how "quotation marks" and "sp|äci|al cħæ¶ä¢ŧ€rß" behave."""
178
ss(s1).should.eql ["Let's", 'check','how', 'quotation marks', 'and', 'sp|äci|al cħæ¶ä¢ŧ€rß', 'behave.']
179
it "correctly splits across line breaks", ->
180
s2 = """this "text in quotes\n with a line-break" ends here"""
181
ss(s2).should.eql ["this", "text in quotes\n with a line-break", "ends", "here"]
182
it "also doesn't stumble over uneven quotations", ->
183
s3 = """1 "a b c" d e f "g h i" "j k"""
184
ss(s3).should.eql ["1", "a b c", "d", "e", "f", "g h i", "j", "k"]
185
186
describe "count", ->
187
cnt = misc.count
188
it "correctly counts the number of occurrences of X in Y", ->
189
X = "bar"
190
Y = "bar batz barbar abar rabarbar"
191
cnt(Y, X).should.be.exactly 6
192
it "counts special characters", ->
193
cnt("we ¢ount ¢oins", "¢").should.eql 2
194
it "returns zero if nothing has been found", ->
195
cnt("'", '"').should.eql 0
196
197
describe "min_object of target and upper_bound", ->
198
mo = misc.min_object
199
upper_bound = {a:5, b:20, xyz:-2}
200
it "modifies target in place", ->
201
target = {a:7, b:15, xyz:5.5}
202
# the return value are just the values
203
mo(target, upper_bound).should.eql [ 5, 15, -2 ]
204
target.should.eql {a:5, b:15, xyz:-2}
205
it "works without a target", ->
206
mo(upper_bounds : {a : 42}).should.be.ok
207
it "returns empty object if nothing is given", ->
208
mo().should.be.eql []
209
210
describe 'merge', ->
211
merge = misc.merge
212
it 'checks that {a:5} merged with {b:7} is {a:5,b:7}', ->
213
merge({a:5},{b:7}).should.eql {a:5,b:7}
214
it 'checks that x={a:5} merged with {b:7} mutates x to be {a:5,b:7}', ->
215
x = {a:5}; merge(x,{b:7})
216
x.should.eql {a:5,b:7}
217
it 'checks that duplicate keys are overwritten by the second entry', ->
218
a = {x:1, y:2}
219
b = {x:3}
220
merge(a, b)
221
a.should.eql {x:3, y:2}
222
it 'variable number of arguments are supported', ->
223
a = {x:1}; b = {y:2}; c = {z:3}; d = {u:4}; w ={v:5, x:0}
224
r = merge(a, b, c, d, w)
225
res = {x:0, y:2, z:3, u:4, v:5}
226
r.should.eql res
227
a.should.eql res
228
229
describe 'cmp', ->
230
cmp = misc.cmp
231
it 'compares 4 and 10 and returns a negative number', ->
232
cmp(4, 10).should.be.below 0
233
it 'compares 10 and 4 and returns a positive number', ->
234
cmp(10, 4).should.be.above 0
235
it 'compares 10 and 10 and returns 0', ->
236
cmp(10, 10).should.be.exactly 0
237
238
describe "walltime functions", ->
239
@t0 = 10000000
240
describe "mswalltime measures in milliseconds", =>
241
it "should be in milliseconds", =>
242
misc.mswalltime().should.be.below 10000000000000
243
it "computes differences", =>
244
misc.mswalltime(@t0).should.be.above 1000000000000
245
describe "walltime measures in seconds", =>
246
it "should be in seconds", =>
247
misc.walltime().should.be.above 1435060052
248
misc.walltime(1000 * @t0).should.be.below 100000000000
249
250
describe "uuid", ->
251
uuid = misc.uuid
252
cnt = misc.count
253
uuid_test = (uid) ->
254
cnt(uid, "-") == 3 and u.length == 36
255
ivuuid = misc.is_valid_uuid_string
256
it "generates random stuff in a certain pattern", ->
257
ids = []
258
for i in [1..100]
259
u = uuid()
260
ids.should.not.containEql u
261
ids.push(u)
262
u.should.have.lengthOf(36)
263
cnt(u, "-").should.be.exactly 4
264
ivuuid(u).should.be.true()
265
266
describe "is_valid_uuid_string", ->
267
ivuuid = misc.is_valid_uuid_string
268
it "checks the UUID pattern", ->
269
ivuuid('C56A4180-65AA-42EC-A945-5FD21DEC').should.be.false()
270
ivuuid("").should.be.false()
271
ivuuid("!").should.be.false()
272
ivuuid("c56a4180-65aa-4\nec-a945-5fd21dec0538").should.be.false()
273
ivuuid("77897c43-dbbc-4672 9a16-6508f01e0039").should.be.false()
274
ivuuid("c56a4180-65aa-42ec-a945-5fd21dec0538").should.be.true()
275
ivuuid("77897c43-dbbc-4672-9a16-6508f01e0039").should.be.true()
276
277
describe "test_times_per_second", ->
278
it "checks that x*x runs really fast", ->
279
misc.times_per_second((x) -> x*x).should.be.greaterThan 10000
280
281
describe "to_json", ->
282
to_json = misc.to_json
283
it "converts a list of objects to json", ->
284
input = ['hello', {a:5, b:37.5, xyz:'123'}]
285
exp = '["hello",{"a":5,"b":37.5,"xyz":"123"}]'
286
to_json(input).should.be.eql(exp).and.be.a.string
287
it "behaves fine with empty arguments", ->
288
to_json([]).should.be.eql('[]')
289
290
describe "from_json", ->
291
from_json = misc.from_json
292
it "parses a JSON string", ->
293
input = '["hello",{"a":5,"b":37.5,"xyz":"123"}]'
294
exp = ['hello', {a:5, b:37.5, xyz:'123'}]
295
from_json(input).should.eql(exp).and.be.an.object
296
it "converts from a string to Javascript and properly deals with ISO dates", ->
297
# TODO what kind of string should this match?
298
dstr = '"2015-01-02T03:04:05+00:00"'
299
exp = new Date(2015, 0, 2, 3, 2, 5)
300
#expect(from_json(dstr)).toBeA(Date).toEqual exp
301
it "throws an error for garbage", ->
302
(-> from_json '{"x": ]').should.throw /^Unexpected token/
303
304
describe "to_safe_str", ->
305
tss = misc.to_safe_str
306
it "removes keys containing pass", ->
307
exp = '{"remove_pass":"(unsafe)","me":"not"}'
308
tss({"remove_pass": "yes", "me": "not"}).should.eql exp
309
it "removes key where the value starts with sha512$", ->
310
exp = '{"delme":"(unsafe)","x":42}'
311
tss({"delme": "sha512$123456789", "x": 42}).should.eql exp
312
it "truncates long string values when serializing an object", ->
313
large =
314
delme:
315
yyyyy: "zzzzzzzzzzzzzz"
316
aaaaa: "bbbbbbbbbbbbbb"
317
ccccc: "dddddddddddddd"
318
eeeee: "ffffffffffffff"
319
keep_me: 42
320
exp = '{"delme":"[object]","keep_me":42}'
321
tss(large).should.be.eql exp
322
323
describe "dict, like in Python", ->
324
dict = misc.dict
325
it "converts a list of tuples to a mapping", ->
326
input = [["a", 1], ["b", 2], ["c", 3]]
327
dict(input).should.eql {"a":1, "b": 2, "c": 3}
328
it "throws on tuples longer than 2", ->
329
input = [["foo", 1, 2, 3]]
330
(-> dict(input)).should.throw /unexpected length/
331
332
describe "remove, like in python", ->
333
rm = misc.remove
334
it "removes the first occurrance in a list", ->
335
input = [1, 2, "x", 8, "y", "x", "zzz", [1, 2], "x"]
336
exp = [1, 2, 8, "y", "x", "zzz", [1, 2], "x"]
337
rm(input, "x")
338
input.should.be.eql exp
339
it "throws an exception if val not in list", ->
340
input = [1, 2, "x", 8, "y", "x", "zzz", [1, 2], "x"]
341
exp = [1, 2, "x", 8, "y", "x", "zzz", [1, 2], "x"]
342
(-> rm(input, "z")).should.throw /item not in array/
343
input.should.eql exp
344
it "works with an empty argument", ->
345
(-> rm([], undefined)).should.throw /item not in array/
346
347
describe "to_iso", ->
348
iso = misc.to_iso
349
it "correctly creates a truncated date string according to the ISO standard", ->
350
d1 = new Date()
351
iso(d1).should.containEql(":").and.containEql(":").and.containEql("T")
352
d2 = new Date(2015, 2, 3, 4, 5, 6)
353
iso(d2).should.eql "2015-03-03T04:05:06"
354
355
describe "is_empty_object", ->
356
ie = misc.is_empty_object
357
it "detects empty objects", ->
358
ie({}).should.be.ok()
359
ie([]).should.be.ok()
360
it "doesn't detect anything else", ->
361
#ie("x").should.not.be.ok()
362
ie({a:5}).should.not.be.ok()
363
ie(b:undefined).should.not.be.ok()
364
#ie(undefined).should.not.be.ok()
365
#ie(null).should.not.be.ok()
366
#ie(false).should.not.be.ok()
367
368
describe "len", ->
369
l = misc.len
370
it "counts the number of keys of an object", ->
371
l({}).should.be.exactly 0
372
l([]).should.be.exactly 0
373
l(a:5).should.be.exactly 1
374
l(x:1, y:[1,2,3], z:{a:1, b:2}).should.be.exactly 3
375
376
describe "keys", ->
377
k = misc.keys
378
it "correctly returns the keys of an object", ->
379
k({a:5, xyz:'10'}).should.be.eql ['a', 'xyz']
380
k({xyz:'10', a:5}).should.be.eql ['xyz', 'a']
381
it "doesn't choke on empty objects", ->
382
k([]).should.be.eql []
383
k({}).should.be.eql []
384
385
describe "pairs_to_obj", ->
386
pto = misc.pairs_to_obj
387
it "convert an array of 2-element arrays to an object", ->
388
pto([['a',5], ['xyz','10']]).should.be.eql({a:5, xyz:'10'}).and.be.an.object
389
it "doesn't fail for empty lists", ->
390
pto([]).should.be.eql({}).and.be.an.object
391
#it "and properly throws errors for wrong arguments", ->
392
# (-> pto [["x", 1], ["y", 2, 3]]).should.throw()
393
394
describe "obj_to_pairs", ->
395
otp = misc.obj_to_pairs
396
it "converts an object to a list of pairs", ->
397
input =
398
a: 12
399
b: [4, 5, 6]
400
c:
401
foo: "bar"
402
bar: "foo"
403
exp = [["a", 12], ["b", [4,5,6]], ["c", {"bar": "foo", "foo": "bar"}]]
404
otp(input).should.be.eql exp
405
406
describe "substring_count", =>
407
@ssc = misc.substring_count
408
@string = "Foofoofoo Barbarbar BatztztztzTatzDatz Katz"
409
@substr1 = "oofoo"
410
@substr2 = "tztz"
411
@substr3 = " "
412
it "number of occurrances of a string in a substring", =>
413
@ssc(@string, @substr1).should.be.exactly 1
414
@ssc(@string, @substr2).should.be.exactly 2
415
@ssc(@string, @substr3).should.be.exactly 2
416
it "number of occurrances of a string in a substring /w overlapping", =>
417
@ssc(@string, @substr1, true).should.be.exactly 2
418
@ssc(@string, @substr2, true).should.be.exactly 3
419
@ssc(@string, @substr3, true).should.be.exactly 3
420
it "counts empty strings", =>
421
@ssc(@string, "").should.be.exactly 47
422
423
424
describe "min/max of array", =>
425
@a1 = []
426
@a2 = ["f", "bar", "batz"]
427
@a3 = [6, -3, 7, 3, -99, 4, 9, 9]
428
it "minimum works", =>
429
misc.min(@a3).should.be.exactly -99
430
it "maximum works", =>
431
misc.max(@a3).should.be.exactly 9
432
it "doesn't work for strings", =>
433
misc.max(@a2).should.be.eql NaN
434
misc.min(@a2).should.be.eql NaN
435
it "fails for empty arrays", =>
436
(-> misc.min(@a1)).should.throw /Cannot read property 'reduce' of undefined/
437
(-> misc.max(@a1)).should.throw /Cannot read property 'reduce' of undefined/
438
439
440
describe "copy flavours:", =>
441
@mk_object= ->
442
o1 = {}
443
o2 = {ref: o1}
444
o = a: o1, b: [o1, o2], c: o2
445
[o, o1]
446
describe "copy", =>
447
c = misc.copy
448
it "creates a shallow copy of a map", =>
449
[o, o1] = @mk_object()
450
co = c(o)
451
co.should.have.properties ["a", "b", "c"]
452
co.a.should.be.exactly o1
453
co.b[0].should.be.exactly o1
454
co.c.ref.should.be.exactly o1
455
456
describe "copy", =>
457
c = misc.copy
458
it "copies a string", =>
459
c("foobar").should.be.exactly "foobar"
460
461
describe "copy_without", =>
462
it "creates a shallow copy of a map but without some keys", =>
463
[o, o1] = @mk_object()
464
co = misc.copy_without(o, "b")
465
co.should.have.properties ["a", "c"]
466
co.a.should.be.exactly o1
467
co.c.ref.should.be.exactly o1
468
469
it "also works for an array of filtered keys", =>
470
[o, o1] = @mk_object()
471
misc.copy_without(o, ["a", "c"]).should.have.properties ["b"]
472
473
it "and doesn't throw for unknown keys", =>
474
# TODO: maybe it should
475
[o, o1] = @mk_object()
476
(-> misc.copy_without(o, "d")).should.not.throw()
477
478
describe "copy_with", =>
479
it "creates a shallow copy of a map but only with some keys", =>
480
[o, o1] = @mk_object()
481
misc.copy_with(o, "a").should.have.properties ["a"]
482
483
it "also works for an array of included keys", =>
484
[o, o1] = @mk_object()
485
co = misc.copy_with(o, ["a", "c"])
486
co.should.have.properties ["a", "c"]
487
co.a.should.be.exactly o1
488
co.c.ref.should.be.exactly o1
489
490
it "and does not throw for unknown keys", =>
491
# TODO: maybe it should
492
[o, o1] = @mk_object()
493
(-> misc.copy_with(o, "d")).should.not.throw()
494
495
describe "deep_copy", =>
496
it "copies nested objects, too", =>
497
[o, o1] = @mk_object()
498
co = misc.deep_copy(o)
499
co.should.have.properties ["a", "b", "c"]
500
501
co.a.should.not.be.exactly o1
502
co.b[0].should.not.be.exactly o1
503
co.c.ref.should.not.be.exactly o1
504
505
co.a.should.be.eql o1
506
co.b[0].should.be.eql o1
507
co.c.ref.should.be.eql o1
508
509
510
it "handles RegExp and Date", =>
511
d = new Date(2015,1,1)
512
# TODO not sure if those regexp modes are copied correctly
513
# this is just a working case, probably not relevant
514
r = new RegExp("x", "gim")
515
o = [1, 2, {ref: [d, r]}]
516
co = misc.deep_copy(o)
517
518
co[2].ref[0].should.be.a.Date
519
co[2].ref[1].should.be.a.RegExp
520
521
co[2].ref[0].should.not.be.exactly d
522
co[2].ref[1].should.not.be.exactly r
523
524
co[2].ref[0].should.be.eql d
525
co[2].ref[1].should.be.eql r
526
527
describe "normalized_path_join", ->
528
pj = misc.normalized_path_join
529
it "Leaves single argument joins untouched", ->
530
pj("lonely").should.be.eql "lonely"
531
532
it "Does nothing with empty strings", ->
533
pj("", "thing").should.be.eql "thing"
534
535
it "Ignores undefined parts", ->
536
pj(undefined, undefined, "thing").should.be.eql "thing"
537
538
it "Does not skip previous upon an absolute path", ->
539
pj("not-skipped!", "/", "thing").should.be.eql "not-skipped!/thing"
540
541
it "Shrinks multiple /'s into one / if found anywhere", ->
542
pj("//", "thing").should.be.eql "/thing"
543
pj("a//", "//", "//thing").should.be.eql "a/thing"
544
pj("slashes////inside").should.be.eql "slashes/inside"
545
546
it "Ignores empty strings in the middle", ->
547
pj("a", "", "thing").should.be.eql "a/thing"
548
549
it "Allows generating absolute paths using a leading /", ->
550
pj("/", "etc", "stuff", "file.name").should.be.eql "/etc/stuff/file.name"
551
552
it "Allows generating a folder path using a trailing /", ->
553
pj("/", "etc", "stuff", "folder/").should.be.eql "/etc/stuff/folder/"
554
pj("/", "etc", "stuff", "folder", "/").should.be.eql "/etc/stuff/folder/"
555
556
557
describe "path_split", ->
558
ps = misc.path_split
559
it "returns {head:..., tail:...} where tail is everything after the final slash", ->
560
ps("/").should.be.eql {head: "", tail: ""}
561
ps("/HOME/USER").should.be.eql {head: "/HOME", tail: "USER"}
562
ps("foobar").should.be.eql {head: "", tail: "foobar"}
563
ps("/home/user/file.ext").should.be.eql {head: "/home/user", tail: "file.ext"}
564
565
566
describe "meta_file", ->
567
mf = misc.meta_file
568
it "constructs a metafile to a given file", ->
569
mf("foo", "history").should.be.eql ".foo.sage-history"
570
mf("/", "batz").should.be.eql "..sage-batz"
571
mf("/home/user/file.ext", "chat").should.be.eql "/home/user/.file.ext.sage-chat"
572
573
574
describe "trunc", ->
575
t = misc.trunc
576
input = "abcdefghijk"
577
it "shortens a string", ->
578
exp = "abcdefg…"
579
t(input, 8).should.be.eql exp
580
it "raises an error when requested length below 1", ->
581
t(input, 1).should.be.eql "…"
582
(-> t(input, 0)).should.throw /must be >= 1/
583
it "defaults to length 1024", ->
584
long = ("x" for [1..10000]).join("")
585
t(long).should.endWith("…").and.has.length 1024
586
it "and handles empty strings", ->
587
t("").should.be.eql ""
588
it "handles missing argument", ->
589
should(t()).be.eql undefined
590
591
describe "trunc_left", ->
592
tl = misc.trunc_left
593
input = "abcdefghijk"
594
it "shortens a string from the left", ->
595
exp = "…efghijk"
596
tl(input, 8).should.be.eql exp
597
it "raises an error when requested length less than 1", ->
598
tl(input, 1).should.be.eql "…"
599
(-> tl(input, 0)).should.throw /must be >= 1/
600
it "defaults to length 1024", ->
601
long = ("x" for [1..10000]).join("")
602
tl(long).should.startWith("…").and.has.length 1024
603
it "handles empty strings", ->
604
tl("").should.be.eql ""
605
it "handles missing argument", ->
606
should(tl()).be.eql undefined
607
608
describe "trunc_middle", ->
609
tl = misc.trunc_middle
610
input = "abcdefghijk"
611
it "shortens a string in middle (even)", ->
612
exp = 'abc…hijk'
613
tl(input, 8).should.be.eql exp
614
it "shortens a string in middle (odd)", ->
615
exp = 'abc…ijk'
616
tl(input, 7).should.be.eql exp
617
it "raises an error when requested length less than 1", ->
618
tl(input, 1).should.be.eql "…"
619
(-> tl(input, 0)).should.throw /must be >= 1/
620
621
describe "git_author", ->
622
it "correctly formats the author tag", ->
623
fn = "John"
624
ln = "Doe"
625
em = "[email protected]"
626
misc.git_author(fn, ln, em).should.eql "John Doe <[email protected]>"
627
628
describe "canonicalize_email_address", ->
629
cea = misc.canonicalize_email_address
630
it "removes +bar@", ->
631
cea("[email protected]").should.be.eql "[email protected]"
632
it "does work fine with objects", ->
633
cea({foo: "bar"}).should.be.eql '{"foo":"bar"}'
634
635
describe "lower_email_address", ->
636
lea = misc.lower_email_address
637
it "converts email addresses to lower case", ->
638
lea("[email protected]").should.be.eql "[email protected]"
639
it "does work fine with objects", ->
640
lea({foo: "bar"}).should.be.eql '{"foo":"bar"}'
641
642
describe "parse_user_search", ->
643
pus = misc.parse_user_search
644
it "reads in a name, converts to lowercase tokens", ->
645
exp = {email_queries: [], string_queries: [["john", "doe"]]}
646
pus("John Doe").should.be.eql exp
647
it "reads in a comma separated list of usernames", ->
648
exp = {email_queries: [], string_queries: [["j", "d"], ["h", "s", "y"]]}
649
pus("J D, H S Y").should.be.eql exp
650
it "reads in a angle bracket wrapped email addresses", ->
651
exp = {email_queries: ["[email protected]"], string_queries: []}
652
pus("<[email protected]>").should.be.eql exp
653
it "reads in email addresses", ->
654
exp = {email_queries: ["[email protected]"], string_queries: []}
655
pus("[email protected]").should.be.eql exp
656
it "also handles mixed queries and spaces", ->
657
exp = {email_queries: ["[email protected]", "[email protected]"], string_queries: [["john", "doe"]]}
658
pus(" [email protected] , John Doe ; <[email protected]>").should.eql exp
659
660
describe "delete_trailing_whitespace", ->
661
dtw = misc.delete_trailing_whitespace
662
it "removes whitespace in a string", ->
663
dtw(" ] łæđ}²đµ· ").should.be.eql " ] łæđ}²đµ·"
664
dtw(" bar ").should.be.eql " bar"
665
dtw("batz ").should.be.eql "batz"
666
dtw("").should.be.eql ""
667
668
describe "misc.assert", ->
669
it "is throws an Error when condition is not met", ->
670
(-> misc.assert(false, new Error("x > 0"))).should.throw "x > 0"
671
it "does nothing when condition is met", ->
672
(-> misc.assert(true, new Error("x < 0"))).should.not.throw()
673
it "is throws a msg wrapped in Error when condition is not met", ->
674
(-> misc.assert(false, "x > 0")).should.throw "x > 0"
675
676
describe "filename_extension", ->
677
fe = misc.filename_extension
678
it "properly returns the remainder of a filename", ->
679
fe("abc.def.ghi").should.be.exactly "ghi"
680
fe("a/b/c/foo.jpg").should.be.exactly "jpg"
681
fe('a/b/c/foo.ABCXYZ').should.be.exactly 'ABCXYZ'
682
it "and an empty string if there is no extension", ->
683
fe("uvw").should.have.lengthOf(0).and.be.a.string
684
fe('a/b/c/ABCXYZ').should.be.exactly ""
685
it "does not get confused by dots in the path", ->
686
fe('foo.bar/baz').should.be.exactly ''
687
fe('foo.bar/baz.ext').should.be.exactly 'ext'
688
689
# TODO not really sure what retry_until_success should actually take care of
690
# at least: the `done` callback of the mocha framework is called inside a passed in cb inside the function f
691
describe "retry_until_success", ->
692
693
beforeEach =>
694
@log = sinon.spy()
695
@fstub = sinon.stub()
696
697
it "calls the function and callback exactly once", (done) =>
698
@fstub.callsArgAsync(0)
699
700
misc.retry_until_success
701
f: @fstub #(cb) => cb()
702
cb: () =>
703
sinon.assert.calledTwice(@log)
704
done()
705
start_delay : 1
706
log : @log
707
708
it "tests if calling the cb with an error is handled correctly", (done) =>
709
# first, calls the cb with something != undefined
710
@fstub.onCall(0).callsArgWithAsync(0, new Error("just a test"))
711
# then calls the cb without anything
712
@fstub.onCall(1).callsArgAsync(0)
713
714
misc.retry_until_success
715
f: @fstub
716
cb: () =>
717
sinon.assert.calledTwice(@fstub)
718
@log.getCall(1).args[0].should.match /err=Error: just a test/
719
@log.getCall(2).args[0].should.match /try 2/
720
done()
721
start_delay : 1
722
log: @log
723
724
it "fails after `max_retries`", (done) =>
725
# always error
726
@fstub.callsArgWithAsync(0, new Error("just a test"))
727
728
misc.retry_until_success
729
f: @fstub
730
cb: () =>
731
@fstub.should.have.callCount 5
732
@log.should.have.callCount 10
733
@log.getCall(1).args[0].should.match /err=Error: just a test/
734
@log.getCall(8).args[0].should.match /try 5\/5/
735
done()
736
start_delay : 1
737
log: @log
738
max_tries: 5
739
740
describe "retry_until_success_wrapper", ->
741
742
it "is a thin wrapper around RetryUntilSuccess", (done) =>
743
ret = misc.retry_until_success_wrapper
744
f: () =>
745
done()
746
ret()
747
748
describe "Retry Until Success", ->
749
# TODO: there is obvisouly much more to test, or to mock-out and check in detail
750
751
it "will retry to execute a function", (done) =>
752
fstub = sinon.stub()
753
fstub.callsArg(0)
754
755
ret = misc.retry_until_success_wrapper
756
f: fstub
757
start_delay : 1
758
759
ret(() =>
760
fstub.should.have.callCount 1
761
done())
762
763
describe "eval_until_defined", ->
764
# TODO
765
766
# TODO: this is just a stub
767
describe "StringCharMapping", ->
768
769
beforeEach =>
770
@scm = new misc.StringCharMapping()
771
772
it "the constructor' intial state", =>
773
@scm._to_char.should.be.empty()
774
@scm._next_char.should.be.eql "B"
775
776
it "works with calling to_string", =>
777
# HSY: this just records what it does
778
@scm.to_string(["A", "K"]).should.be.eql "BC"
779
780
describe "uniquify_string", ->
781
it "removes duplicated characters", ->
782
s = "aabb ŋ→wbſß?- \nccccccccc\txxxöä"
783
res = misc.uniquify_string(s)
784
exp = "ab ŋ→wſß?-\nc\txöä"
785
res.should.eql exp
786
787
describe "PROJECT_GROUPS", ->
788
it "checks that there has not been an accedental edit of this array", ->
789
act = misc.PROJECT_GROUPS
790
exp = ['owner', 'collaborator', 'viewer', 'invited_collaborator', 'invited_viewer']
791
act.should.be.eql exp
792
793
describe "make_valid_name", ->
794
it "removes non alphanumeric chars to create an identifyer fit for using in an URL", ->
795
s = "make_valid_name øf th1s \nſŧ¶→”ŋ (without) chöcking on spe\tial ¢ħæ¶æ¢ŧ€¶ſ"
796
act = misc.make_valid_name(s)
797
exp = "make_valid_name__f_th1s__________without__ch_cking_on_spe_ial___________"
798
act.should.be.eql(exp).and.have.length exp.length
799
800
describe "parse_bup_timestamp", ->
801
it "reads e.g. 2014-01-02-031508 and returns a date object", ->
802
input = "2014-01-02-031508"
803
act = misc.parse_bup_timestamp("2014-01-02-031508")
804
act.should.be.instanceOf Date
805
exp = new Date('2014-01-02T03:15:08.000Z')
806
act.should.be.eql exp
807
808
describe "hash_string", ->
809
hs = misc.hash_string
810
it "returns 0 for an empty string", ->
811
hs("").should.be.exactly 0
812
it "deterministically hashes a string", ->
813
s1 = "foobarblablablaöß\næ\tx"
814
h1 = hs(s1)
815
h1.should.be.eql hs(s1)
816
for i in [2..s1.length-1]
817
hs(s1.substring(i)).should.not.be.eql h1
818
819
describe "parse_hashtags", ->
820
ph = misc.parse_hashtags
821
it "returns empty array for nothing", ->
822
ph().should.eql []
823
it "returns empty when no valid hashtags", ->
824
ph("no hashtags here!").length.should.be.exactly 0
825
it "returns empty when empty string", ->
826
ph("").length.should.be.exactly 0
827
it "returns correctly for one hashtag", ->
828
ph("one #hashtag here").should.eql [[4, 12]]
829
it "works for many hashtags in one string", ->
830
ph("#many #hashtags here #should #work").should.eql [[0, 5], [6, 15], [21, 28], [29, 34]]
831
it "makes sure hash followed by noncharacter is not a hashtag", ->
832
ph("#hashtag # not hashtag ##").should.eql [[0,8]]
833
834
describe "mathjax_escape", ->
835
me = misc.mathjax_escape
836
it "correctly escapes the right characters", ->
837
me("& < > \" \'").should.eql "&amp; &lt; &gt; &quot; &#39;"
838
it "doesn't escape already escaped sequences", ->
839
me("&dont;escape").should.eql "&dont;escape"
840
841
describe "path_is_in_public_paths", ->
842
p = misc.path_is_in_public_paths
843
it "returns false for a path with no public paths", ->
844
p("path", []).should.be.false()
845
it "returns false if path is undefined and there are no public paths -- basically avoid possible hack", ->
846
p(null, []).should.be.false()
847
it "returns false if path is undefined and there is a public path -- basically avoid possible hack", ->
848
p(null, ["/public/path"]).should.be.false()
849
it "returns true if the entire project is public", ->
850
p("path", [""]).should.be.true()
851
it "returns true if the path matches something in the list", ->
852
p("path", ["path_name", "path"]).should.be.true()
853
it "returns true if the path is within a public path", ->
854
p("path/name", ["path_name", "path"]).should.be.true()
855
it "returns true if path ends with .zip and is within a public path", ->
856
p("path/name.zip", ["path_name", "path"]).should.be.true()
857
it "handles path.zip correctly if it is not in the path", ->
858
p("foo/bar.zip", ["foo/baz"]).should.be.false()
859
it "returns false if the path is not in the public paths", ->
860
p("path", ["path_name", "path/name"]).should.be.false()
861
it "doesn't allow relativ path trickery", ->
862
p("../foo", ["foo"]).should.be.false()
863
864
865
describe "call_lock", =>
866
before =>
867
@clock = sinon.useFakeTimers()
868
869
after =>
870
@clock.restore()
871
872
beforeEach =>
873
@objspy = sinon.spy()
874
@o = obj: @objspy, timeout_s: 5
875
876
it "adds a call lock to a given object", =>
877
misc.call_lock(@o)
878
@objspy.should.have.properties ["_call_lock", "_call_unlock", "_call_with_lock"]
879
880
fspy = sinon.spy()
881
@objspy._call_with_lock(fspy)
882
@objspy.should.have.properties __call_lock: true
883
fspy.should.have.callCount 1
884
885
fspy2 = sinon.spy()
886
cbspy2 = sinon.spy()
887
@objspy._call_with_lock(fspy2, cbspy2)
888
889
# check that the cb has been called with the error message
890
cbspy2.getCall(0).args[0].should.eql "error -- hit call_lock"
891
# and the function hasn't been called
892
fspy2.should.have.callCount 0
893
894
it "unlocks after the given timeout_s time", =>
895
misc.call_lock(@o)
896
897
fspy = sinon.spy()
898
@objspy._call_with_lock(fspy)
899
900
# turn clock 6 secs ahead
901
@clock.tick 6*1000
902
fspy3 = sinon.spy()
903
cbspy3 = sinon.spy()
904
@objspy._call_with_lock(fspy3, cbspy3)
905
906
cbspy3.should.have.callCount 0
907
fspy3.should.have.callCount 1
908
909
it "unlocks when function is called", =>
910
fcl = misc.call_lock(@o)
911
912
fspy = sinon.spy()
913
cbspy2 = sinon.spy()
914
f = () -> fspy()
915
@objspy._call_with_lock(f, cbspy2)
916
917
cbspy2.should.have.callCount 0
918
fspy.should.have.callCount 1
919
920
# TODO I have no idea how to actually call it in such a way,
921
# that this is false
922
@objspy.should.have.properties __call_lock: true
923
924
925
describe "timestamp_cmp", ->
926
tcmp = misc.timestamp_cmp
927
a = timestamp: new Date("2015-01-01")
928
b = timestamp: new Date("2015-01-02")
929
930
it "correctly compares timestamps", ->
931
tcmp(a, b).should.eql 1
932
tcmp(b, a).should.eql -1
933
# sometimes, that's -0 instead of 0
934
assert.strictEqual(tcmp(a, a), 0)
935
936
it "handles missing timestamps gracefully", ->
937
tcmp(a, {}).should.eql -1
938
tcmp({}, b).should.eql 1
939
940
describe "ActivityLog", =>
941
beforeEach =>
942
# e1 and e2 are deliberately on the same file
943
@e1 =
944
id: "1234"
945
timestamp: new Date("2015-01-01T12:34:55")
946
project_id: "c26db83a-7fa2-44a4-832b-579c18fac65f"
947
path: "foo/bar.baz"
948
949
@e2 =
950
id: "2345"
951
timestamp: new Date("2015-01-02T12:34:56")
952
project_id: "c26db83a-7fa2-44a4-832b-579c18fac65f"
953
path: "foo/bar.baz"
954
955
@e3 =
956
id: "3456"
957
timestamp: new Date("2015-01-01T12:34:55")
958
project_id: "c26db83a-7fa2-44a4-832b-579c18fac65f"
959
path: "x/y.z"
960
action: 'c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz'
961
seen_by: "123456789"
962
read_by: "123456789"
963
964
@al = misc.activity_log
965
events: [@e1, @e2, @e3]
966
account_id: "123456789"
967
notifications: {}
968
969
describe "constructor", =>
970
it "works correctly", =>
971
@al.should.have.properties
972
notifications:
973
'c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz':
974
id: '2345'
975
timestamp: new Date("2015-01-02T12:34:56")
976
'c26db83a-7fa2-44a4-832b-579c18fac65f/x/y.z':
977
id: '3456'
978
timestamp: new Date("2015-01-01T12:34:55")
979
"c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz":
980
"undefined": new Date("2015-01-01T12:34:55")
981
read: new Date("2015-01-01T12:34:55")
982
seen: new Date("2015-01-01T12:34:55")
983
account_id: "123456789"
984
985
describe "obj", =>
986
it "returns a map with the last notification", =>
987
@al.obj().should.eql
988
notifications:
989
"c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz":
990
id: "2345"
991
timestamp: new Date("2015-01-02T12:34:56")
992
"c26db83a-7fa2-44a4-832b-579c18fac65f/x/y.z":
993
id: "3456"
994
timestamp: new Date("2015-01-01T12:34:55")
995
"c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz":
996
"undefined": new Date("2015-01-01T12:34:55")
997
read: new Date("2015-01-01T12:34:55")
998
seen: new Date("2015-01-01T12:34:55")
999
account_id: "123456789"
1000
1001
describe "process", =>
1002
it "correctly processes additional events", =>
1003
@al.process([
1004
id: "4567"
1005
timestamp: new Date("2015-01-03T12:34:56")
1006
project_id: "c26db83a-7fa2-44a4-832b-579c18fac65h"
1007
path: "x/y.z"
1008
])
1009
@al.notifications.should.eql
1010
"c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz":
1011
id: "2345"
1012
timestamp: new Date("2015-01-02T12:34:56")
1013
"c26db83a-7fa2-44a4-832b-579c18fac65f/x/y.z":
1014
id: "3456"
1015
timestamp: new Date("2015-01-01T12:34:55")
1016
"c26db83a-7fa2-44a4-832b-579c18fac65f/foo/bar.baz":
1017
"undefined": new Date("2015-01-01T12:34:55")
1018
read: new Date("2015-01-01T12:34:55")
1019
seen: new Date("2015-01-01T12:34:55")
1020
"c26db83a-7fa2-44a4-832b-579c18fac65h/x/y.z":
1021
id: "4567"
1022
timestamp: new Date("2015-01-03T12:34:56")
1023
1024
describe "encode_path", ->
1025
e = misc.encode_path
1026
it "escapes # and ?", ->
1027
e("file.html?param#anchor").should.eql "file.html%3Fparam%23anchor"
1028
it "doesn't escape other path characters", ->
1029
e("a/b,&$:@=+").should.eql "a/b,&$:@=+"
1030
1031
1032
describe "remove_c_comments", ->
1033
r = misc.remove_c_comments
1034
it "removes a /* c style */ comment", ->
1035
r("start/* remove me */ end").should.eql "start end"
1036
it "doesn't touch a normal string", ->
1037
r("foo").should.eql "foo"
1038
it "removes multiple comments in one string", ->
1039
r("/* */foo/*remove*/bar").should.eql "foobar"
1040
it "discards one-sided comments", ->
1041
r("foo /* bar").should.be.eql "foo /* bar"
1042
r("foo */ bar").should.be.eql "foo */ bar"
1043
r("foo */ bar /* baz").should.be.eql "foo */ bar /* baz"
1044
1045
1046
describe "capitalize", ->
1047
c = misc.capitalize
1048
it "capitalizes the first letter of a word", ->
1049
c("foo").should.eql "Foo"
1050
it "works with non ascii characters", ->
1051
c("å∫ç").should.eql "Å∫ç"
1052
1053
describe "parse_mathjax returns list of index position pairs (i,j)", ->
1054
pm = misc.parse_mathjax
1055
it "but no indices when called on nothing", ->
1056
pm().should.eql []
1057
it "correctly for $", ->
1058
pm("foo $bar$ batz").should.eql [[4, 9]]
1059
it "works regarding issue #1795", ->
1060
pm("$x_{x} x_{x}$").should.eql [[0, 13]]
1061
it "ignores single $", ->
1062
pm("$").should.eql []
1063
pm("the amount is $100").should.eql []
1064
pm("a $b$ and $").should.eql [[2, 5]]
1065
pm("a $b$ and $ ignored").should.eql [[2, 5]]
1066
it "correctly works for multiline strings", ->
1067
s = """
1068
This is a $formula$ or a huge $$formula$$
1069
\\begin{align}
1070
formula
1071
\\end{align}
1072
\\section{that's it}
1073
"""
1074
pm(s).should.be.eql([[ 10, 19 ], [ 30, 41 ], [ 42, 75 ]])
1075
.and.matchEach (x) -> s.slice(x[0], x[1]).should.containEql "formula"
1076
it "detects brackets", ->
1077
s = "\\(foo\\) and \\[foo\\]"
1078
pm(s).should.eql([[0, 7], [12, 19]])
1079
.and.matchEach (x) -> s.slice(x[0]+2, x[1]-2).should.eql "foo"
1080
it "works for other environments", ->
1081
pm("\\begin{equation}foobar\\end{equation}").should.eql [[0, 36]]
1082
pm("\\begin{equation*}foobar\\end{equation*}").should.eql [[0, 38]]
1083
pm('\\begin{align}foobar\\end{align}').should.eql [[0, 30]]
1084
pm('\\begin{align*}foobar\\end{align*}').should.eql [[0, 32]]
1085
pm('\\begin{eqnarray}foobar\\end{eqnarray}').should.eql [[0, 36]]
1086
pm('\\begin{eqnarray*}foobar\\end{eqnarray*}').should.eql [[0, 38]]
1087
pm('\\begin{bmatrix}foobar\\end{bmatrix}').should.eql [[0, 34]]
1088
it "is not triggered by unknown environments", ->
1089
pm('\\begin{cmatrix}foobar\\end{cmatrix}').should.eql []
1090
1091
describe "replace_all", ->
1092
ra = misc.replace_all
1093
it "replaces all occurrences of a string in a string", ->
1094
ra("foobarbaz", "bar", "-").should.eql "foo-baz"
1095
ra("x y z", " ", "").should.eql "xyz"
1096
ra(ra("foo\nbar\tbaz", "\n", ""), "\t", "").should.eql "foobarbaz"
1097
ra("ſþ¨€¢→æł ¢ħæ¶æ¢ŧ€¶ſ", "æ", "a").should.eql "ſþ¨€¢→ał ¢ħa¶a¢ŧ€¶ſ"
1098
1099
1100
#describe "stripe_date", ->
1101
# sd = misc.stripe_date
1102
# it "creates a 'stripe date' (?) out of a timestamp (seconds since epoch)", ->
1103
# sd(1000000000).should.containEql('Sunday')
1104
# .containEql('September')
1105
# .containEql("9")
1106
# .containEql('2001')
1107
1108
1109
describe "date_to_snapshot_format", ->
1110
dtsf = misc.date_to_snapshot_format
1111
it "correctly converts a number-date to the snapshot format", ->
1112
dtsf(1000000000000).should.be.eql "2001-09-09-014640"
1113
it "assumes timestamp 0 for no argument", ->
1114
dtsf().should.be.eql "1970-01-01-000000"
1115
it "works correctly for Date instances", ->
1116
dtsf(new Date("2015-01-02T03:04:05+0600")).should.be.eql "2015-01-01-210405"
1117
1118
describe "smileys", ->
1119
it "replaces strings", ->
1120
misc.smiley(s : "hey :-) you !!! :-)").should.be.eql "hey 😁 you !!! 😁"
1121
it "wraps for html", ->
1122
res = misc.smiley
1123
s : "foo :-) bar"
1124
wrap : ["<span class='x'>", "</span>"]
1125
res.should.be.eql "foo <span class='x'>😁</span> bar"
1126
1127
describe "human readable list", ->
1128
thl = misc.to_human_list
1129
it "handles small lists", ->
1130
thl([]).should.be.eql ""
1131
it "single value lists", ->
1132
thl([1]).should.be.eql "1"
1133
it "converts longer lists well", ->
1134
arr = ["a", ["foo", "bar"], 99]
1135
exp = 'a, foo,bar and 99'
1136
thl(arr).should.be.eql exp
1137
1138
describe "peer grading", ->
1139
peer_grading = misc.peer_grading
1140
it "sometimes throws errors", ->
1141
expect(-> peer_grading([1,2,3], N=0)).toThrow()
1142
expect(-> peer_grading([1,2,3], N=1)).toNotThrow()
1143
expect(-> peer_grading([1,2,3], N=2)).toNotThrow()
1144
expect(-> peer_grading([1,2,3], N=3)).toThrow()
1145
expect(-> peer_grading([1,2,3], N=4)).toThrow()
1146
1147
it "generates proper peer lists", ->
1148
for n in [1..5]
1149
for s in [(n+1)...20]
1150
students = ("S_#{i}" for i in [0...s])
1151
asmnt = peer_grading(students, N=n)
1152
1153
expect(students).toEqual misc.keys(asmnt)
1154
expect(misc.keys(asmnt).length).toEqual s
1155
1156
for k, v of asmnt
1157
# check student not assigned to him/herself
1158
assert v.indexOf(k) == -1
1159
# check all assigments have N students ...
1160
assert v.length == n
1161
# ... and do not contain duplicates
1162
assert underscore.uniq(v).length == v.length
1163
# and each student has to grade n times
1164
for s in students
1165
c = underscore.filter(
1166
v.indexOf(s) for _, v of asmnt,
1167
(x) -> x != -1).length
1168
expect(c).toEqual n
1169
1170
describe "sum", ->
1171
it "adds up an array", ->
1172
expect(misc.sum([1,2,3])).toEqual 6
1173
it "works with empty arrays", ->
1174
expect(misc.sum([])).toEqual 0
1175
it "has an option to set a start", ->
1176
expect(misc.sum([-1,5], start=-5)).toEqual -1
1177
1178
describe "ticket_id_to_ticket_url", ->
1179
t2t = misc.ticket_id_to_ticket_url
1180
it "converts a number or string to an url", ->
1181
x = t2t(123)
1182
x.should.match /^http/
1183
x.should.match /123/
1184
y = t2t("123")
1185
y.should.match /^http/
1186
y.should.match /123/
1187
1188
describe "map_limit limits the values of a by the values in b or by b if b is a number", ->
1189
it "Limits by a map with similar keys", ->
1190
a = {'x': 8, 'y': -1, 'z': 5}
1191
b = {'x': 4.4, 'y': 2.2}
1192
e = {'x': 4.4, 'y': -1, 'z': 5}
1193
misc.map_limit(a, b).should.eql e
1194
it "Limits by a number", ->
1195
a = {'x': 8, 'y': -1, 'z': 5}
1196
b = 0
1197
e = {'x': 0, 'y': -1, 'z': 0}
1198
misc.map_limit(a, b).should.eql e
1199
1200
describe 'is_valid_email_address is', ->
1201
valid = misc.is_valid_email_address
1202
it "true for [email protected]", ->
1203
valid('[email protected]').should.be.true()
1204
it "false for blabla", ->
1205
valid('blabla').should.be.false()
1206
1207
describe 'separate_file_extension', ->
1208
sfe = misc.separate_file_extension
1209
it "splits filename.ext accordingly", ->
1210
{name, ext} = sfe('foobar/filename.ext')
1211
name.should.be.eql "foobar/filename"
1212
ext.should.be.eql "ext"
1213
it "ignores missing extensions", ->
1214
{name, ext} = sfe('foo.bar/baz')
1215
name.should.be.eql 'foo.bar/baz'
1216
ext.should.be.eql ''
1217
1218
describe 'change_filename_extension', ->
1219
cfe = misc.change_filename_extension
1220
it "changes a tex to pdf", ->
1221
cfe('filename.tex', 'pdf').should.be.exactly 'filename.pdf'
1222
cfe('/bar/baz/foo.png', 'gif').should.be.exactly '/bar/baz/foo.gif'
1223
it "deals with missing extensions", ->
1224
cfe('filename', 'tex').should.be.exactly 'filename.tex'
1225
1226
describe 'path_to_tab', ->
1227
it "appends editor- to the front of the string", ->
1228
misc.path_to_tab('str').should.be.exactly 'editor-str'
1229
1230
describe 'tab_to_path', ->
1231
it "returns undefined if given undefined", ->
1232
should(misc.tab_to_path()).be.undefined()
1233
it "returns undefined if given a non-editor name", ->
1234
should(misc.tab_to_path("non-editor")).be.undefined()
1235
it "returns the string truncating editor-", ->
1236
misc.tab_to_path("editor-path/name.thing").should.be.exactly "path/name.thing"
1237
1238
describe 'suggest_duplicate_filename', ->
1239
dup = misc.suggest_duplicate_filename
1240
it "works with numbers", ->
1241
dup('filename-1.test').should.be.eql 'filename-2.test'
1242
dup('filename-99.test').should.be.eql 'filename-100.test'
1243
dup('filename_001.test').should.be.eql 'filename_2.test'
1244
dup('filename_99.test').should.be.eql 'filename_100.test'
1245
it "works also without", ->
1246
dup('filename-test').should.be.eql 'filename-test-1'
1247
dup('filename-xxx.test').should.be.eql 'filename-xxx-1.test'
1248
dup('bla').should.be.eql 'bla-1'
1249
dup('foo.bar').should.be.eql 'foo-1.bar'
1250
it "also works with weird corner cases", ->
1251
dup('asdf-').should.be.eql 'asdf--1'
1252
1253
describe 'top_sort', ->
1254
# Initialize DAG
1255
DAG =
1256
node1 : []
1257
node0 : []
1258
node2 : ["node1"]
1259
node3 : ["node1", "node2"]
1260
old_DAG_string = JSON.stringify(DAG)
1261
1262
it 'Returns a valid ordering', ->
1263
expect misc.top_sort(DAG)
1264
.toEqual ['node1', 'node0', 'node2', 'node3'] or
1265
['node0', 'node1', 'node2', 'node3']
1266
1267
it 'Omits graph sources when omit_sources:true', ->
1268
expect misc.top_sort(DAG, omit_sources:true)
1269
.toEqual ['node2', 'node3']
1270
1271
it 'Leaves the original DAG the same afterwards', ->
1272
misc.top_sort(DAG)
1273
expect JSON.stringify(DAG)
1274
.toEqual old_DAG_string
1275
1276
DAG2 =
1277
node0 : []
1278
node1 : ["node2"]
1279
node2 : ["node1"]
1280
1281
it 'Detects cycles and throws an error', ->
1282
expect(() => misc.top_sort(DAG2)).toThrow("Store has a cycle in its computed values")
1283
1284
DAG3 =
1285
node1 : ["node2"]
1286
node2 : ["node1"]
1287
1288
it 'Detects a lack of sources and throws an error', ->
1289
expect () => misc.top_sort(DAG3)
1290
.toThrow("No sources were detected")
1291
1292
describe 'create_dependency_graph', ->
1293
store_def =
1294
first_name : => "Joe"
1295
last_name : => "Smith"
1296
full_name : (first_name, last_name) => "#{@first_name} #{@last_name}"
1297
short_name : (full_name) => @full_name.slice(0,5)
1298
1299
store_def.full_name.dependency_names = ['first_name', 'last_name']
1300
store_def.short_name.dependency_names = ['full_name']
1301
1302
DAG_string = JSON.stringify
1303
first_name : []
1304
last_name : []
1305
full_name : ["first_name", "last_name"]
1306
short_name : ["full_name"]
1307
1308
it 'Creates a DAG with the right structure', ->
1309
expect JSON.stringify(misc.create_dependency_graph(store_def))
1310
.toEqual DAG_string
1311
1312
describe 'bind_objects', ->
1313
scope =
1314
get : () -> "cake"
1315
value : "cake"
1316
1317
obj1 =
1318
func11: () -> @get()
1319
func12: () -> @value
1320
1321
obj2 =
1322
func21: () -> @get()
1323
func22: () -> @value
1324
1325
obj1.func11.prop = "cake"
1326
1327
result = misc.bind_objects(scope, [obj1, obj2])
1328
1329
it 'Binds all functions in a list of objects of functions to a scope', ->
1330
for obj in result
1331
for key, val of obj
1332
expect(val()).toEqual("cake")
1333
1334
it 'Leaves the original object unchanged', ->
1335
expect(=> obj1.func11()).toThrow(/get is not a function/)
1336
1337
it 'Preserves the toString of the original function', ->
1338
expect(result[0].func11.toString()).toEqual(obj1.func11.toString())
1339
1340
it 'Preserves properties of the original function', ->
1341
expect(result[0].func11.prop).toEqual(obj1.func11.prop)
1342
1343
it 'Ignores non-function values', ->
1344
scope =
1345
value : "cake"
1346
1347
obj1 =
1348
val : "lies"
1349
func: () -> @value
1350
[b_obj1] = misc.bind_objects(scope, [obj1])
1351
1352
expect(b_obj1.func()).toEqual("cake")
1353
expect(b_obj1.val).toEqual("lies")
1354
1355
describe 'test the date parser --- ', ->
1356
it 'a date with a zone', ->
1357
expect(misc.date_parser(undefined, "2016-12-12T02:12:03.239Z") - 0).toEqual(1481508723239)
1358
1359
it 'a date without a zone (should default to utc)', ->
1360
expect(misc.date_parser(undefined, "2016-12-12T02:12:03.239") - 0).toEqual(1481508723239)
1361
1362
it 'a date without a zone and more digits (should default to utc)', ->
1363
expect(misc.date_parser(undefined, "2016-12-12T02:12:03.239417") - 0).toEqual(1481508723239)
1364
1365
it 'a non-date does nothing', ->
1366
expect(misc.date_parser(undefined, "cocalc")).toEqual('cocalc')
1367
1368
describe 'test ISO_to_Date -- ', ->
1369
expect(misc.ISO_to_Date("2016-12-12T02:12:03.239Z") - 0).toEqual(1481508723239)
1370
1371
it 'a date without a zone (should default to utc)', ->
1372
expect(misc.ISO_to_Date("2016-12-12T02:12:03.239") - 0).toEqual(1481508723239)
1373
1374
it 'a date without a zone and more digits (should default to utc)', ->
1375
expect(misc.ISO_to_Date("2016-12-12T02:12:03.239417") - 0).toEqual(1481508723239)
1376
1377
it 'a non-date does NaN', ->
1378
expect(isNaN(misc.ISO_to_Date("cocalc"))).toEqual(true)
1379
1380
1381
describe 'test converting to and from JSON for sending over a socket -- ', ->
1382
it 'converts object involving various timestamps', ->
1383
obj = {first:{now:new Date()}, second:{a:new Date(0), b:'2016-12-12T02:12:03.239'}}
1384
expect(misc.from_json_socket(misc.to_json_socket(obj))).toEqual(obj)
1385
1386
describe 'misc.transform_get_url mangles some URLs or "understands" what action to take', ->
1387
turl = misc.transform_get_url
1388
it 'preserves "normal" URLs', ->
1389
turl('http://example.com/file.tar.gz').should.eql {command:'wget', args:["http://example.com/file.tar.gz"]}
1390
turl('https://example.com/file.tar.gz').should.eql {command:'wget', args:["https://example.com/file.tar.gz"]}
1391
turl('https://raw.githubusercontent.com/lightning-viz/lightning-example-notebooks/master/index.ipynb').should.eql
1392
command:'wget'
1393
args:['https://raw.githubusercontent.com/lightning-viz/lightning-example-notebooks/master/index.ipynb']
1394
it 'handles git@github urls', ->
1395
u = turl('[email protected]:sagemath/sage.git')
1396
u.should.eql {command: 'git', args: ["clone", "[email protected]:sagemath/sage.git"]}
1397
it 'understands github "blob" urls', ->
1398
# branch
1399
turl('https://github.com/sagemath/sage/blob/master/README.md').should.eql
1400
command: 'wget'
1401
args: ['https://raw.githubusercontent.com/sagemath/sage/master/README.md']
1402
# specific commit
1403
turl('https://github.com/sagemath/sage/blob/c884e41ac51bb660074bf48cc6cb6577e8003eb1/README.md').should.eql
1404
command: 'wget'
1405
args: ['https://raw.githubusercontent.com/sagemath/sage/c884e41ac51bb660074bf48cc6cb6577e8003eb1/README.md']
1406
it 'git-clones everything that ends with ".git"', ->
1407
turl('git://trac.sagemath.org/sage.git').should.eql
1408
command: 'git'
1409
args: ['clone', 'git://trac.sagemath.org/sage.git']
1410
it 'and also git-clonse https:// addresses', ->
1411
turl('https://github.com/plotly/python-user-guide').should.eql
1412
command: 'git'
1413
args: ['clone', 'https://github.com/plotly/python-user-guide.git']
1414
it 'also knows about some special URLs', ->
1415
# github
1416
turl('http://nbviewer.jupyter.org/github/lightning-viz/lightning-example-notebooks/blob/master/index.ipynb').should.eql
1417
command: 'wget'
1418
args: ['https://raw.githubusercontent.com/lightning-viz/lightning-example-notebooks/master/index.ipynb']
1419
# url → http
1420
turl('http://nbviewer.jupyter.org/url/jakevdp.github.com/downloads/notebooks/XKCD_plots.ipynb').should.eql
1421
command: 'wget'
1422
args: ['http://jakevdp.github.com/downloads/notebooks/XKCD_plots.ipynb']
1423
# note, this is urls → https
1424
turl('http://nbviewer.jupyter.org/urls/jakevdp.github.com/downloads/notebooks/XKCD_plots.ipynb').should.eql
1425
command: 'wget'
1426
args: ['https://jakevdp.github.com/downloads/notebooks/XKCD_plots.ipynb']
1427
# github gist -- no idea how to do that
1428
#turl('http://nbviewer.jupyter.org/gist/darribas/4121857').should.eql
1429
# command: 'wget'
1430
# args: ['https://gist.githubusercontent.com/darribas/4121857/raw/505e030811332c78e8e50a54aca5e8034605cb4c/guardian_gaza.ipynb']
1431
1432
1433
1434
1435