function tests(property, propEqual, jsc, arbs, xmlParser) { // simple utility functions function words(s) { return _.words(s, /[^\s]+/g); } function compose(f, g) { return x => f(g(x)); } function vectorSum(arr1,arr2) { return _.zipWith(arr1, arr2, _.add); } window.vectorSum = vectorSum // how to generate sentences (for mkPrettyPrinter & mkIndenter) let sentence = // words are 2-12 lower letters arbs.sentence(arbs.word(arbs.lLetter,2,12), // prefer white space between words arbs.oneOfWeight([arbs.whitespace, 8], // but rarely put '\n' [jsc.constant('\n'), 1]), // prefer no leading whitespace arbs.oneOfWeight([jsc.constant(''), 3], // but sometimes add some [arbs.whitespace, 1]), // slightly prefer adding trailing whitespace arbs.oneOfWeight([arbs.whitespace, 2], // but sometimes don't [jsc.constant(''), 1]), // use '.', '!', '?' or '...' at the end arbs.punctuation, // sentence has 5 to 30 words 5, 30); test("Scalar product", function() { property( "doesn't change the source array", "array number", "number", function (arr,num) { let copy = _.cloneDeep(arr); let t = scalar_product(copy,num); return propEqual(copy, arr, `scalar_product([${copy}],${num})`, `[${arr}]`); }); property( "preserves the length of the array", "array number", "number", function (arr,num) { return propEqual(scalar_product(arr,num).length, arr.length, `scalar_product([${arr}],${num}).length`, `${arr}.length`); }); property( "returns zero array if the factor is zero", "array number", function (arr) { let z = _.cloneDeep(arr); _.fill(z,0); return propEqual(scalar_product(arr,0),z, `scalar_product([${arr}],0)`, `[${z}]`); }); property( "preserves the array if the factor is one", "array number", function (arr) { let c = _.cloneDeep(arr); return propEqual(scalar_product(arr,1),c, `scalar_product([${arr}],1))`, `[${c}]`); }); // have to restrict it to integers because of the rounding errors // better to compare up to rounding error property( "allows factors to be combined", "array integer", "integer", "integer", function (arr, n1, n2) { return propEqual(scalar_product(scalar_product(arr,n1),n2), scalar_product(arr, n1*n2), `scalar_product(scalar_product([${arr}],${n1}),${n2})`, `scalar_product([${arr}], ${n1}*${n2})`); }); property( "returns undefined if first argument is not an array", jsc.suchthat(arbs.anything, x => !_.isArray(x)), arbs.anything, function(notArr, x) { return propEqual(scalar_product(notArr, x), undefined, `scalar_product(${notArr}, ${x})`, `undefined`); }); property( "if factor is not passed, array is returned as is", "array number", function(arr) { return propEqual(scalar_product(arr), arr, `scalar_product([${arr}])`, `[${arr}]`); }); }); test("Inner product", function() { property( "doesn't change the source arrays", arbs.equalLengthArrays(jsc.number, jsc.number), function (arrs) { let [a1,a2] = arrs; let copy1 = _.cloneDeep(a1); let copy2 = _.cloneDeep(a2); var t = inner_product(a1,a2); return propEqual(a1,copy1) && propEqual(a2,copy2); }); property( "is symmetric", arbs.equalLengthArrays(jsc.number, jsc.number), function (arrs) { let [a1,a2] = arrs; return propEqual(inner_product(a1,a2), inner_product(a2,a1), `inner_product([${a1}],[${a2}])`, `inner_product([${a2}],[${a1}])`); }); property( "returns non-negative number when multiplying vector to itself", "array number", function (arr) { return inner_product(arr,arr) >= 0; }); property( "returns 0 if one of the vectors is a zero-filled [0,...,0] vector", "array number", function (arr) { let z = new Array(arr.length); _.fill(z,0); return propEqual(inner_product(arr,z), 0, `inner_product([${arr}],[${z}])`, `0`); }); property( "if argument arrays sizes don't match, returns undefined", jsc.array(arbs.anything), function(arr1) { return jsc.forall( jsc.suchthat(jsc.array(arbs.anything), x => x.length !== arr1.length), function(arr2) { return propEqual(inner_product(arr1,arr2), undefined, `inner_product([${arr1}],[${arr2}])` , `undefined`); }); }); }); test("mapReduce", function() { // mapReduce is pretty agnostic to the array contents // so we could use "json" arbitrary instead of "number" // to get better coverage // Unfortunately, it is also quite slower property( "doesn't change the source array", "array integer", "integer -> integer", function (arr, f) { let copy = _.cloneDeep(arr); let t = mapReduce(copy, f); return propEqual(copy, arr, `[${copy}]`,`[${arr}]`); }); property( "mapReduce([],f,g,seed) == seed", "integer -> integer", arbs.fn2(jsc.integer), "integer", function(f, combine, seed) { return propEqual(mapReduce([],f,combine,seed), seed, `mapReduce([],${f},${combine},${seed})`, `${seed}`); }); property( "mapReduce(arr,identity,combine,seed) == reduce(arr,combine,seed)", "array integer", arbs.fn2(jsc.integer), "integer", function (arr, combine, seed) { return propEqual(mapReduce(arr,_.identity,combine,seed), _.reduce(arr,combine,seed), `mapReduce([${arr}],${_.identity},${combine},${seed})`, `_.reduce([${arr}],${combine},${seed})`); }); property( "mapReduce(arr,f,combine,seed) == " + "mapReduce(arr,identity,(a,x) => combine(a,f(x)),f(seed))", "array integer", "integer -> integer", arbs.fn2(jsc.integer), "integer", function (arr, f, combine, seed) { return propEqual( mapReduce(arr,f,combine,seed), mapReduce(arr,_.identity,(a,x) => combine(a,f(x)),seed), `mapReduce([${arr}],${f},${combine},${seed})`, `mapReduce([${arr}],${_.identity},(a,x) => ${combine}(a,${f}(x)),${seed})`); }); property( "mapReduce(arr1.concat(arr2),f) == mapReduce(arr1,f) + mapReduce(arr2,f)", "array integer", "array integer", "integer -> integer", function (arr1, arr2, f) { return propEqual( mapReduce(arr1.concat(arr2),f), mapReduce(arr1,f) + mapReduce(arr2,f), `mapReduce([${arr1}].concat([${arr2}]),${f})`, `mapReduce([${arr1}],${f}) + mapReduce([${arr2}],${f})`); }); property( "mapReduce(arr1.concat(arr2),f,+,0) == " + "mapReduce(arr1,f,+,0) + mapReduce(arr2,f,+,0)", "array integer", "array integer", "integer -> integer", function (arr1, arr2, f) { return propEqual( mapReduce(arr1.concat(arr2),f, _.add, 0), mapReduce(arr1,f,_.add,0) + mapReduce(arr2,f,_.add,0), `mapReduce([${arr1}].concat([${arr2}]),${f}, ${_.add}, 0)`, `mapReduce([${arr1}],${f},${_.add},0) + mapReduce([${arr2}],${f},${_.add},0)`); }); property( "mapReduce(arr,f,(a,x) => a.concat([x]),[]) == map(arr,f)", "array integer", "integer -> integer", function (arr, f) { return propEqual(mapReduce(arr,f,(a,x) => a.concat([x]), []), _.map(arr,f), `mapReduce([${arr}],${f},(a,x) => a.concat([x]), [])`, `${_.map(arr,f)}`); }); property( "mapReduce(arr,f,(a,x) => a.concat(x),[]) == flatMap(arr,f)", "array integer", "integer -> array integer", function (arr,f) { return propEqual(mapReduce(arr,f,(a,x) => a.concat(x),[]), _.flatMap(arr,f), `mapReduce([${arr}],${f},(a,x) => a.concat(x),[])`, `[${_.flatMap(arr,f)}]`); }); property( "mapReduce([1,2,3],f,g,0) == g(g(g(0,f(1)),f(2)),f(3))", "integer -> integer", arbs.fn2(jsc.integer), function (f, g) { return propEqual(mapReduce([1,2,3],f,g,0), g(g(g(0,f(1)),f(2)),f(3)), `mapReduce([1,2,3],${f},${g},0)`, `${g(g(g(0,f(1)),f(2)),f(3))}`); }); property( "if the first argument is not an array, returns undefined", jsc.suchthat(arbs.anything, x => !Array.isArray(x)), "number -> number", function(notArr, f) { return propEqual(mapReduce(notArr,f), undefined); }); property( "if the second argument is not a function, returns undefined", "array integer", jsc.suchthat(arbs.anything, x => typeof x !== 'function'), function(arr, notF) { return propEqual(mapReduce(arr,notF), undefined); }); }); test("mergeSortedArrays", function() { property( "doesn't change the source arrays", arbs.sortedArray(jsc.integer), arbs.sortedArray(jsc.integer), function(a, b) { let copy1 = _.cloneDeep(a); let copy2 = _.cloneDeep(b); let t = mergeSortedArrays(copy1,copy2); return propEqual(copy1,a) && propEqual(copy2,b); }); property( "produces sorted results", arbs.sortedArray(jsc.integer), arbs.sortedArray(jsc.integer), function(a, b) { let t = mergeSortedArrays(a,b); return propEqual(t, _.sortBy(t), `[${t}]`,`mergeSortedArrays([${a}],[${b}])`); }); property( "has length equal to the sum of individual lengths", arbs.sortedArray(jsc.integer), arbs.sortedArray(jsc.integer), function(a, b) { return propEqual(mergeSortedArrays(a,b).length, a.length + b.length, `mergeSortedArrays([${a}],[${b}]).length`, `[${a}].length + [${b}].length`); }); }); test("range", function() { property( "results in undefined if step is zero", "integer", "integer", function(x,y) { return propEqual(range(x,y,0), undefined, `range(${x},${y},0)`, undefined, `[${range(x,y,0)}]`); }); property( "results in empty array if x > y and step > 0 or x < y and step < 0", "integer", function(x) { return jsc.forall( jsc.suchthat(jsc.integer, y => y != x), arbs.posInteger, function(y,stepMod) { let s = x>y ? stepMod : -stepMod return propEqual(range(x,y,s), [], `range(${x},${y},${s})`, undefined, `[${range(x,y,s)}]`); }); }); property( "produces array without repetitions", "integer", "integer", jsc.suchthat(jsc.integer, x => x != 0), function(x,y,step) { let r = range(x,y,step); return propEqual(r.length, _.uniq(r).length, `range(${x},${y},${step})`, undefined, `[${range(x,y,step)}]`); }); property( "produces array with all elements >= min(x,y)", "integer", "integer", jsc.suchthat(jsc.integer, x => x != 0), function(x,y,step) { let r = range(x,y,step); let l = _.min([x,y]); return propEqual(_.every(r,a => a >= l), true, `range(${x},${y},${step})`, undefined, `[${range(x,y,step)}]`); }); property( "produces array with all elements <= max(x,y)", "integer", "integer", jsc.suchthat(jsc.integer, x => x != 0), function(x,y,step) { let r = range(x,y,step); let h = _.max([x,y]); return propEqual(_.every(r,a => a <= h), true, `range(${x},${y},${step})`, undefined, `[${range(x,y,step)}]`); }); property( "returns undefined, if either argument is not a number", "number", "number", jsc.suchthat(jsc.number, x => x != 0), jsc.suchthat(arbs.anything, x => !_.isNumber(x) || _.isNaN(x)), jsc.suchthat(arbs.anything, x => !_.isNumber(x) || _.isNaN(x)), jsc.suchthat(arbs.anything, x => !_.isNumber(x) || _.isNaN(x)), jsc.suchthat(arbs.anything, x => !_.isUndefined(x) && (!_.isNumber(x) || _.isNaN(x))), function(x,y,step,nN1,nN2,nN3,nN4) { return propEqual(range(nN1,nN2,step), undefined) && propEqual(range(nN1,y,step), undefined) && propEqual(range(x,nN2,step), undefined) && propEqual(range(nN1,nN2,nN3), undefined) && propEqual(range(x,y,nN4), undefined) && propEqual(range(nN1,y,nN3), undefined) && propEqual(range(x,nN2,nN3), undefined); }); }); test("flatten", function() { property( "doesn't change the input array", "array json", function(a) { let copy = _.cloneDeep(a); let t = flatten(copy); return propEqual(copy, a, `flatten([${a}])`); }); property( "simply returns the input, if it is not an array", jsc.oneof(jsc.number, jsc.bool, jsc.falsy, jsc.string), function(inp) { return propEqual(flatten(inp), inp, `flatten(${inp})`); }); property( "simply returns the input, if it is already flattened", jsc.array(jsc.oneof(jsc.number, jsc.bool, jsc.falsy, jsc.string)), function(a) { return propEqual(flatten(a), a, `flatten([${a}])`, `[${a}]`); }); property( "flatten([arr]) == flatten(arr)", "array json", function(a) { return propEqual(flatten([a]), flatten(a), `flatten([[${a}]])`, `flatten([${a}])`); }); }); test("mkPrettyPrinter", function() { property( "output has no too long lines", jsc.integer(20,80), jsc.array(sentence), function(ls, input) { let inp = input.join('\n'); let pp = mkPrettyPrinter(ls); let r = pp(inp); return propEqual(_.every(r, l => l.length <= ls), true, `mkPrettyPrinter(${ls})("${inp.replace(/\n/, '\\n')}")`, undefined, `[${mkPrettyPrinter(ls)(inp)}]`); }); property( "output has all input words in the same order", jsc.integer(20,80), jsc.array(sentence), function(ls, input) { let inp = input.join('\n'); let ws = words(inp); let pp = mkPrettyPrinter(ls); let r = pp(inp); return propEqual(words(r.join('\n')), ws, `mkPrettyPrinter(${ls})("${inp.replace(/\n/, '\\n')}")`, undefined); }); property( "output has no leading or trailing white spaces", jsc.integer(20,80), jsc.array(sentence), function(ls, input) { let inp = input.join('\n'); let pp = mkPrettyPrinter(ls); let r = pp(inp); return propEqual(_.every(r, l => (l == _.trim(l))), true, `mkPrettyPrinter(${ls})("${inp.replace(/\n/, '\\n')}")`, undefined, `[${mkPrettyPrinter(ls)(inp)}]`); }); property( "does not use global variables", arbs.vect(jsc.integer(20,80),5), arbs.permutation(5), jsc.array(sentence), function(lss, perm, input) { let inp = input.join('\n'); let pps = lss.map(mkPrettyPrinter); for (let i in perm) { let j = perm[i]; let r = pps[j](inp); _.every(r, l => propEqual(l.length <= lss[j], true)); pps.splice(j,1); lss.splice(j,1); } return true; }); property( "returns undefined if called with not a number", jsc.suchthat(arbs.anything, x => !_.isUndefined(x) && (!_.isNumber(x) || _.isNaN(x))), function(nN) { return propEqual(mkPrettyPrinter(nN), undefined, `mkPrettyPrinter(${nN})`); }); property( "if pretty-printer is called with non-string, it returns undefined", jsc.integer(50,80), jsc.suchthat(arbs.anything, x => !_.isString(x)), function(ls, nStr) { return propEqual(mkPrettyPrinter(ls)(nStr), undefined, `mkPrettyPrinter(${ls})(${nStr})`); }); }); test("mkIndenter", function() { let indenterInput = jsc.oneof(jsc.nat(20), sentence, jsc.array(sentence)); // it's easier to generate absolute indentation levels // and then turn them into relative ones function fixIndentationLevels(arr,curLvl) { for (let i in arr) { if (typeof arr[i] == "number") { let t = arr[i]; arr[i] -= curLvl; curLvl = t; } } return curLvl; } function properlyIndented(il) { return l => { let b = l.substr(0,il); let e = l.slice(il); return propEqual(b, ' '.repeat(il)) && propEqual(e, _.trim(e)) && ! _.includes(e, '\n') && ! _.includes(e, '\t') && ! _.includes(e, ' '); }; } function properlyIndentedLI(il) { return (l,ix) => { let b = l.substr(0,il+2); let e = l.slice(il+2); let expectedIndent = i ? ' '.repeat(il+2) : ' '.repeat(il) + '* '; return propEqual(b, expectedIndent) && propEqual(e, _.trim(e)) && ! _.includes(e, '\n') && ! _.includes(e, '\t') && ! _.includes(e, ' '); }; } function noEarlyWraps(ls, ss, rl, el, ol) { let lss = _.initial(ss).map(x => x.length); let nexts = _.tail(ss).map(x => words(x)[0].length); return propEqual(_.every(_.zipWith(lss,nexts,_.add), s => s >= ls),true,rl,el,ol); } property( "returned function doesn't change its argument", jsc.integer(50,80), jsc.nat(12), jsc.array(sentence), function(ls, il, arr) { let copy = _.cloneDeep(arr); let i = mkIndenter(ls, il); let t = i(copy); return propEqual(copy,arr, `mkIndenter(${ls}, ${il})([${arr}])`); }); property( "freshly created indenter returns indentation level passed " + "during creation", jsc.integer(50,80), jsc.nat(20), function(ls, il) { let i = mkIndenter(ls, il); return propEqual(i(true), il, `mkIndenter(${ls}, ${il})(true)`); }); property( "freshly created indenter returns an empty array", jsc.integer(50,80), jsc.nat(20), function(ls, il) { let i = mkIndenter(ls, il); return propEqual(i(), [], `mkIndenter(${ls}, ${il})()`); }); property( "content request doesn't change the indentation level", jsc.integer(50,80), jsc.nat(20), jsc.array(indenterInput), function(ls, il, arr) { fixIndentationLevels(arr,il); let i = mkIndenter(ls, il); arr.forEach(i); let il2 = i(true); i(); return propEqual(i(true), il2, `var i = mkIndenter(${ls}, ${il});i(true);i(true)`); }); property( "output doesn't have too long lines", jsc.integer(50,80), jsc.nat(20), jsc.array(indenterInput), function(ls, il, arr) { fixIndentationLevels(arr,il); let i = mkIndenter(ls, il); arr.forEach(i); let r = i(); return propEqual(_.every(r, l => l.length <= ls), true, `mkIndenter(${ls}, ${il})()`); }); property( "integer input doesn't produce any output and changes the " + "indentation level", jsc.integer(50,80), jsc.nat(12), jsc.integer(-12,12), function(ls, il, int) { let i = mkIndenter(ls, il); i(int); return propEqual(i(true), il + int, `var i = mkIndenter(${ls}, ${il}); i(${int}); i(true); i()`) && propEqual(i(), [], `var i = mkIndenter(${ls}, ${il}); i(${int}); i(true); i()`); }); property( "string input produces properly indented results " + "and doesn't change the indentation level", jsc.integer(50,80), jsc.nat(20), sentence, function(ls, il, s) { let i = mkIndenter(ls, il); i(s); let r = i(); return propEqual(_.every(r, properlyIndented(il)), true, `var i = mkIndenter(${ls}, ${il}); i("${s}"); i(); i(true)`) && propEqual(i(true), il, `var i = mkIndenter(${ls}, ${il}); i("${s}"); i(); i(true)`); }); property( "string input doesn't wrap lines too early", jsc.integer(50,80), jsc.nat(20), sentence, function(ls, il, s) { let i = mkIndenter(ls, il); i(s); let r = i(); return noEarlyWraps(ls, r, `var i = mkIndenter(${ls}, ${il}); i("${s.replace(/\n/, '\\n')}"); i()`, undefined, `[${i()}]`); }); property( "empty array input does nothing", jsc.integer(50,80), jsc.nat(20), function(ls, il) { let i = mkIndenter(ls, il); i([]); return propEqual(i(), [], `var i = mkIndenter(${ls}, ${il}); i([]); i(); i(true)`) && propEqual(i(true), il, `var i = mkIndenter(${ls}, ${il}); i([]); i(); i(true)`); }); property( "it doesn't use global variables", jsc.integer(2,5), function(n) { let inputsArb = arbs.vect(jsc.tuple([jsc.integer(50,80), jsc.nat(20), jsc.array(sentence)]), n); return jsc.forall( inputsArb, function(inputs) { let seqOuts = _.map(inputs, inp => { let i = mkIndenter(inp[0],inp[1]); inp[2].forEach(i); return i(); }); let sz = _.sum(_.map(inputs, x => x[2].length)) + 2*n; return jsc.forall( arbs.vect(jsc.nat(n-1), sz), function(schedule) { let cnts = _.fill(Array(n), -1); let outs = _.fill(Array(n), undefined); let is = []; schedule.forEach(i_ => { let i = i_; for (; cnts[i] > inputs[i][2].length; i++, i %= n); let inp = inputs[i]; if (cnts[i] == -1) is[i] = mkIndenter(inp[0], inp[1]); else if (cnts[i] == inp[2].length) outs[i] = is[i](); else is[i](inp[2][cnts[i]]); cnts[i]++; }); let os = _.zip(seqOuts, outs); return _.every(os, o => propEqual(o[0], o[1])); }); }); }); property( "returns undefined if either argument is not a number", jsc.suchthat(arbs.anything, x => !_.isNumber(x) || _.isNaN(x)), jsc.suchthat(arbs.anything, x => !_.isUndefined(x) && (!_.isNumber(x) || _.isNaN(x))), jsc.integer(50,80), jsc.nat(20), function(nN1, nN2, ls, il) { return propEqual(mkIndenter(nN1), undefined) && propEqual(mkIndenter(nN1,il), undefined) && propEqual(mkIndenter(ls,nN2), undefined) && propEqual(mkIndenter(nN1,nN2), undefined); }); property( "sample input works", "unit", function(u) { let i = mkIndenter(20); i("Short line"); i("This line is longer than 20 characters"); i(8); i("Big indent"); i("Not so long line"); i(-6); i("Small indent"); i(["Item 1", "Item 2", "Really really long Item 3"]); return propEqual(i(), [ 'Short line', 'This line is longer', 'than 20 characters', ' Big indent', ' Not so long', ' line', ' Small indent', ' * Item 1', ' * Item 2', ' * Really really', ' long Item 3' ]); }); }); test("letter_frequency", function() { property( "Concatenation of strings produces merge of maps", jsc.array(arbs.letter), jsc.array(arbs.letter), function(arr1, arr2) { let s1 = arr1.join(''); let s2 = arr2.join(''); let expected = _.assignWith(letter_frequency(s1), letter_frequency(s2), _.add); return propEqual(letter_frequency(s1 + s2), expected); }); property( "Map values are always positive", jsc.array(arbs.letter), function(arr) { let s = arr.join(''); let r = letter_frequency(s); return _.every(r, x => x > 0); }); property( "Hello example works", "unit", function(u) { let a = []; a.H = 1; a.E = 1; a.L = 2; a.O = 1; let expected = {'H' : 1,'E' : 1,'L' : 2,'O' : 1} try { propEqual(letter_frequency('Hello'), expected) } catch(e) { return propEqual(letter_frequency('Hello'),a) } try { propEqual(letter_frequency('Hello'),a) } catch(e) { return propEqual(letter_frequency('Hello'), expected) } return true }); }); test("display_letter_frequency", function() { // how to generate frequency table let fTable = jsc.array(jsc.tuple([arbs.uLetter, arbs.posInteger])).smap( _.fromPairs, _.toPairs); property( "first arguments is not modified", fTable, function(ft) { let copy = _.cloneDeep(ft); let dom = {}; display_letter_frequency(copy, dom); return propEqual(copy, ft); }); property( "Output is a table with right number of rows and 2 columns", fTable, function(ft) { let dom = {}; display_letter_frequency(ft, dom); let out = xmlParser.parseFromString(dom.innerHTML, 'text/xml'); let d = out.documentElement; let cs = d.children; return propEqual(d.tagName, 'table') && propEqual(cs.length, _.size(ft)) && _.every(_.range(cs.length).map(i => { let c = cs.item(i); let subcs = c.children; return propEqual(c.tagName, 'tr') && propEqual(subcs.length, 2) && _.every(_.range(subcs.length).map(j => { let subc = subcs.item(j); return propEqual(subc.tagName, 'td') && propEqual(subc.children.length, 0); })); })); }); property( "Every row has key/value pair from the frequency table", fTable, function(ft) { function isInMap(k,v) { let i = parseInt(v); return propEqual(i + '', v) && propEqual(ft[k], i); } let dom = {}; display_letter_frequency(ft, dom); let out = xmlParser.parseFromString(dom.innerHTML, 'text/xml'); let d = out.documentElement; let cs = d.children; return propEqual(d.tagName, 'table') && _.every(_.range(cs.length).map(i => { let c = cs.item(i); let subcs = c.children; return propEqual(c.tagName, 'tr') && propEqual(subcs.length,2) && propEqual(subcs.item(0).tagName, 'td') && propEqual(subcs.item(1).tagName, 'td') && isInMap(subcs.item(0).innerHTML, subcs.item(1).innerHTML); })); }); property( "Does nothing if either argument is not an object", fTable, jsc.dict(arbs.anything), jsc.suchthat(arbs.anything, x => !_.isObject(x)), jsc.suchthat(arbs.anything, x => !_.isObject(x)), jsc.suchthat(arbs.anything, x => !_.isObject(x)), function(ft, nO1, nO2, nO3, o) { let copyO = _.cloneDeep(o); let copyNO2 = _.cloneDeep(nO2); let copyNO3 = _.cloneDeep(nO3); display_letter_frequency(nO1, copyNO2); display_letter_frequency(nO1, copyO); display_letter_frequency(ft, copyNO3); return propEqual(copyNO2, nO2) && propEqual(copyO, o) && propEqual(copyNO3, nO3); }); }); }