-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathutils.js
235 lines (205 loc) · 7.11 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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
import chalk from 'chalk'
import { diffStringsUnified } from 'jest-diff'
import engine from './methods.js'
import { serialize } from './snapshot.js'
import fc from 'fast-check'
import { ConstantFunc } from './symbols.js'
const basicSub = { var: '' }
// The following is used as a "simple way" to keep track of whether a complex structure generated from arbitraries is actually constant.
// This allows us to avoid a bunch of duplicate test effort when someone writes something like:
// { name: 'Jesse', image: #commonPicture }, where #commonPicture is defined as a constant.
const constantStructures = new Set()
/**
* Checks if a function in the Logic Engine is logged as constant.
* @param {string} name
* @returns {boolean}
*/
function checkConstantFunction (name) {
return engine.methods[name] && engine.methods[name].method && engine.methods[name].method[ConstantFunc]
}
/**
* It takes the output of the diff function and if it contains the string "Comparing two different
* types", it replaces the output with a more detailed message
* @param expected - The expected value.
* @param received - The value that was actually returned from the test.
* @returns The function diffTouchup is being returned.
*/
export function diff (expected, received) {
if (typeof expected !== typeof received) {
return `Comparing two different types of values. Expected ${chalk.green(typeof expected)} but received ${chalk.red(typeof received)}.\n${chalk.green(`- Expected: ${serialize(expected).replace(/\n/g, '\n ')}`)}\n${chalk.red(`- Received: ${serialize(received).replace(/\n/g, '\n ')}`)}`
}
const result = diffStringsUnified(
serialize(expected),
serialize(received),
{
changeColor: i => i
}
)
return result
}
const tupleConstApplication = item => {
if (item && typeof item === 'object') {
const key = Object.keys(item)[0]
if (key.startsWith('#')) {
return item
}
}
return { '#constant': item }
}
const simplifyArbitraries = new Set(['#oneof'])
/**
* Used to touch up the logic so that statements that aren't completely valid with arbitraries are made valid,
* like #record({ id: #integer }) and { id: #integer } become the same.
* @param {*} item
* @returns
*/
function touchUpArbitrary (item, force = false) {
if (!item) return item
if (typeof item === 'number') return item
if (typeof item === 'string') return item
if (typeof item === 'boolean') return item
if (typeof item === 'bigint') return item
const key = Object.keys(item)[0]
if (key.startsWith('#')) {
if (item[key] && item[key].obj) {
return {
[key]: {
obj: item[key].obj.map(touchUpArbitrary)
}
}
}
if (item[key] && item[key].list) {
return {
[key]: {
list: item[key].list.map(touchUpArbitrary)
}
}
}
// todo: improve the testing around this
if (item[key] && item[key].map && simplifyArbitraries.has(key)) {
return {
[key]: item[key].map(touchUpArbitrary).map(i => {
// detect if any are not objects and wrap them in #constant
if (typeof i !== 'object') return { '#constant': i }
if (i === null) return { '#constant': null }
if (i === undefined) return { '#constant': undefined }
return touchUpArbitrary(i, true)
})
}
}
}
if (key === 'obj') {
if (force === true || item[key].some(i => (Object.keys(i || {})[0] || '').startsWith('#'))) {
const result = {
'#record': {
obj: item[key]
.map(touchUpArbitrary)
// apply a fix to the values of the obj so that they're all wrapped in #constant,
// this makes the sugar simpler.
.map((i, x) => (x & 1) ? tupleConstApplication(i) : i)
}
}
// detect & track if the result is purely constant.
if (result['#record'].obj.every((i, x) => !(x & 1) || constantStructures.has(i) || typeof i['#constant'] !== 'undefined' || checkConstantFunction(Object.keys(i)[0]))) {
constantStructures.add(result)
}
return result
}
}
if (key === 'list') {
if (force === true || item[key].some(i => (Object.keys(i || {})[0] || '').startsWith('#'))) {
const result = {
'#tuple': {
list: item[key]
.map(touchUpArbitrary)
// makes the sugar simpler by applying #constant to the constant values of an inferred tuple.
.map(tupleConstApplication)
}
}
// detect & track if the result is purely constant.
if (result['#tuple'].list.every((i) => typeof i['#constant'] !== 'undefined' || checkConstantFunction(Object.keys(i)[0]))) {
constantStructures.add(result)
}
return result
}
}
const count = arbitraryBranchCount(item)
if (count === 0) {
return item
} else if (count === 1) {
const [map, arbitraries] = traverseSubstitute(item)
return {
...arbitraries[0],
map
}
} else throw new Error('You may not use more than one arbitrary in an argument, did you intend to use #record or #tuple?')
}
/**
* Count the number of references to arbitraries in the logic tree.
* (Nested Arbitraries still count as one, because it terminates the traversal
* when it reaches one; this is by design!).
* @param {*} obj
*/
function arbitraryBranchCount (obj) {
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
const key = Object.keys(obj)[0]
if (key.startsWith('#')) return 1
if (!Array.isArray(obj[key])) {
if (obj[key] && typeof obj[key] === 'object') {
return arbitraryBranchCount(obj[key])
}
return 0
}
return obj[key].reduce((acc, i) => {
acc += arbitraryBranchCount(i)
return acc
}, 0)
}
return 0
}
/**
* Substitute any arbitraries with a "var" expression so that we can perform a map operation
* after building the relevant logic. :)
* @param {*} obj
* @param {*} sub
*/
export function traverseSubstitute (obj, sub = []) {
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
const key = Object.keys(obj)[0]
if (key.startsWith('#')) {
sub.push(obj)
return [basicSub, sub]
}
return [{
[key]: Array.isArray(obj[key]) ? obj[key].map(i => traverseSubstitute(i, sub)[0]) : traverseSubstitute(obj[key], sub)[0]
}, sub]
}
return [obj, sub]
}
/**
* Convert the arguments for a test case into arbitraries.
* @param {...any} args
*/
export async function argumentsToArbitraries (data, ...args) {
args = args.map(touchUpArbitrary)
let constant = true
const list = []
for (const i of args) {
if (i && typeof i === 'object') {
const keys = Object.keys(i)
if (keys[0].startsWith('#')) {
if (!constantStructures.has(i) && keys[0] !== '#constant' && !checkConstantFunction(keys[0])) constant = false
const result = await (await engine.build(i))(data)
if (keys[1] === 'map' && i.map !== basicSub) {
list.push(result.map(engine.fallback.build(i.map)))
continue
}
list.push(result)
continue
}
}
list.push(fc.constant(await (await engine.build(i))(data)))
}
list.constant = constant
return list
}