-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgrain-test.gr
373 lines (330 loc) · 13 KB
/
grain-test.gr
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
/**
* @module grain-test: A simple testing framework for grain
*
* version 0.1.0; works with Grain v0.5
*/
import Option from "option"
import Result from "result"
import String from "string"
import List from "list"
import Array from "array"
import Process from "sys/process"
let detectEnvVar = (var) => Array.contains("GRAIN_TEST_" ++ var ++ "=true", match (Process.env()) {
Ok(envArray) => envArray,
Err(_) => [>]
})
let runningInTesterScript = detectEnvVar("SCRIPT")
let plainOutput = detectEnvVar("PLAIN_OUTPUT")
let onlyFailing = detectEnvVar("ONLY_FAILING")
let bailUponFailure = detectEnvVar("BAIL_UPON_FAILURE")
let red = (msg) => if (!plainOutput) "\x1b[31m" ++ msg ++ "\x1b[0m" else msg
let green = (msg) => if (!plainOutput) "\x1b[32m" ++ msg ++ "\x1b[0m" else msg
let cyan = (msg) => if (!plainOutput) "\x1b[36m" ++ msg ++ "\x1b[0m" else msg
let mut failedAssertions = []
let mut encounteredFailure = false
let failedMessage = (currTest) => red((if (!plainOutput) "✗ " else "failed: ") ++ currTest)
let passedMessage = (currTest) => green((if (!plainOutput) "✓ " else "passed: ") ++ currTest)
let executeTest = (testSuiteName, testName, beforeEaches, afterEaches, runTest) => {
// short-circuit if the --bail-upon-failure flag was set by the test runner and a previous test failed
if (bailUponFailure && encounteredFailure) {
false
} else {
List.forEach((fn) => fn(), beforeEaches)
failedAssertions = []
runTest()
List.forEach((fn) => fn(), afterEaches)
let testPassed = List.length(failedAssertions) == 0
if (testPassed) {
if (!onlyFailing) {
print(passedMessage(testName) ++ "\n")
}
} else {
print(failedMessage(testName))
List.forEach(m => print(" " ++ red(if (!plainOutput) "● " else "- ") ++ m), List.reverse(failedAssertions))
print("")
encounteredFailure = true
}
if (runningInTesterScript) {
// somewhat hacky solution to get number of tests passed/failed to be read when running from the tester script
print("___RUNNING_IN_SCRIPT_TEST_" ++ (if (testPassed) "PASSED" else "FAILED") ++ "_MARKER___")
}
testPassed
}
}
let executeTestMultiple = (testSuiteName, testMultName, beforeEaches, afterEaches, runs, runTest) => {
let testsPassed = List.mapi((run, i) => {
let testName = testMultName ++ " - run " ++ toString(i + 1) ++ " (test data: " ++ toString(run) ++ ")"
executeTest(testSuiteName, testName, beforeEaches, afterEaches, () => runTest(run))
}, runs)
List.every(identity, testsPassed)
}
/**
* An `enum` of all of the possible values that can be included in a test suite.
*
* Use `BeforeEach` to run a function before each test in the suite.
*
* Use `AfterEach` to run a function after each test in the suite.
*
* Use `BeforeAll` to run a function before running the tests in the test suite. Note: `BeforeAll` will run before the first `BeforeEach` if both are given.
*
* Use `AfterAll` to run a function after running all the tests in the test suite. Note: `AfterAll` will run after the last `AfterEach` if both are given.
*
* Use `Test` to run a test as part of a test suite. Behavior is similar to the standalone `test` function.
*
* Use `TestMultiple` to run a test function against multiple inputs. Behavior is similar to the standalone `testMultiple` function.
*/
export enum TestSuiteItem<a> {
BeforeEach(() -> Void),
AfterEach(() -> Void),
BeforeAll(() -> Void),
AfterAll(() -> Void),
Test(String, () -> Void),
TestMultiple(String, List<a>, (a) -> Void)
}
/**
* Run a test suite, defined by a list of `TestSuiteItem`s that are run as part of the suite.
*
* @param testSuiteName: the name of the test suite
* @param testSuiteItem: a list of test suite items that encompass the test suite
*/
export let testSuite = (testSuiteName, testSuiteItems) => {
let suiteIntro = "---- Test suite " ++ testSuiteName ++ " ----"
print("---- Test suite " ++ cyan(testSuiteName) ++ " ----\n")
let (beforeEaches, afterEaches, beforeAlls, afterAlls, tests) = List.reduceRight(
(item, (be, ae, ba, aa, tests)) => match (item) {
BeforeEach(fn) => ([fn, ...be], ae, ba, aa, tests),
AfterEach(fn) => (be, [fn, ...ae], ba, aa, tests),
BeforeAll(fn) => (be, ae, [fn, ...ba], aa, tests),
AfterAll(fn) => (be, ae, ba, [fn, ...aa], tests),
x => (be, ae, ba, aa, [x, ...tests])
},
([], [], [], [], []),
testSuiteItems
)
List.forEach((fn) => fn(), beforeAlls)
let testsPassed = List.map((test) => match (test) {
Test(testName, runTest) => executeTest(Some(testSuiteName), testName, beforeEaches, afterEaches, runTest),
TestMultiple(testMultName, runs, runTest) => executeTestMultiple(Some(testSuiteName), testMultName, beforeEaches, afterEaches, runs, runTest),
_ => false
}, tests)
let testSuitePassed = List.every(identity, testsPassed)
List.forEach((fn) => fn(), afterAlls)
print(Array.reduce((s, _) => s ++ "-", "", String.explode(suiteIntro)) ++ "\n")
if (runningInTesterScript) {
// somewhat hacky solution to get number of test suites passed/failed to be read when running from the tester script
print("___RUNNING_IN_SCRIPT_TEST_SUITE_" ++ (if (testSuitePassed) "PASSED" else "FAILED") ++ "_MARKER___")
}
}
/**
* Run a single test case, consisting of zero or more assertions
*
* @param testName: a description of what the test is doing
* @param runTest: a function that runs the test case
*/
export let test = (testName, runTest) => {
executeTest(None, testName, [], [], runTest)
}
/**
* Run a test with a number of different inputs and expected outputs
*
* @param testMultName: a description of what the test is doing
* @param runs: a list of data to run the test against. Each item will get passed to the test function and can then be referenced in the test
* @param runTest: a function that runs the test case
*/
export let testMultiple = (testMultName, runs, runTest) => {
executeTestMultiple(None, testMultName, [], [], runs, runTest)
}
/**
* Info about the status of an assertion made during a test
*/
export record AssertionInfo {
passed: Bool,
computeFailMsg: () -> String
}
let evaluatedMatcher = (passed, computeFailMsg) => {
{ passed, computeFailMsg }
}
let colorValue = (value) => cyan(toString(value))
/**
* A matcher creator function that checks if two values are equal to each other
*
* @param other: the value the matcher will compare against
*
* @returns a matcher that succeeds if the value being matched against is equal to the value given to create the matcher
*/
export let equals = (other) => (value) => evaluatedMatcher(
other == value,
() => colorValue(value) ++ " to equal " ++ colorValue(other)
)
/**
* A matcher creator function that checks if two values are not equal to each other
*
* @param other: the value the matcher will compare against
*
* @returns a matcher that succeeds if the value being matched against is not equal to the value given to create the matcher
*/
export let notEquals = (other) => (value) => evaluatedMatcher(
other != value,
() => colorValue(value) ++ " not to equal " ++ colorValue(other)
)
/**
* A matcher function that checks if a value is `true`
*
* @returns a matcher that succeeds if the value being matched against is `true`
*/
export let isTrue = (value) => evaluatedMatcher(
value == true,
() => colorValue(value) ++ " to be " ++ colorValue("true")
)
/**
* A matcher function that checks if a value is `false`
*
* @returns a matcher that succeeds if the value being matched against is `false`
*/
export let isFalse = (value) => evaluatedMatcher(
value == false,
() => colorValue(value) ++ " to be " ++ colorValue("false")
)
/**
* A matcher function that checks if an `Option` is `None`
*
* @returns a matcher that succeeds if the `Option` value being matched against is `None`
*/
export let isNone = (value) => evaluatedMatcher(
Option.isNone(value),
() => colorValue(value) ++ " to be a " ++ colorValue("None") ++ " Option"
)
/**
* A matcher function that checks if an `Option` is contentful i.e. the `Some` variant
*
* @returns a matcher that succeeds if the `Option` value being matched against is contentful
*/
export let isSome = (value) => evaluatedMatcher(
Option.isSome(value),
() => colorValue(value) ++ " to be a " ++ colorValue("Some") ++ " Option"
)
/**
* A matcher function that checks if a `Result` is the `Ok` variant
*
* @returns a matcher that succeeds if the `Result` value being matched against is the `Ok` variant
*/
export let isOk = (value) => evaluatedMatcher(
Result.isOk(value),
() => colorValue(value) ++ " to be an " ++ colorValue("Ok") ++ " Result"
)
/**
* A matcher function that checks if a `Result` is the `Err` variant
*
* @returns a matcher that succeeds if the `Result` value being matched against is the `Err` variant
*/
export let isErr = (value) => evaluatedMatcher(
Result.isErr(value),
() => colorValue(value) ++ " to be an " ++ colorValue("Err") ++ " Result"
)
let matchMultiple = (matchers, matchAll) => (value) => {
let (aggregateFn, joinDelim, prefixIfTwo) = if (matchAll) {
(List.every, " and ", "both ")
} else {
(List.some, " or ", "either ")
}
let evaluated = List.map((fn) => fn(value), matchers)
evaluatedMatcher(
aggregateFn((test) => test.passed, evaluated),
() => (if (List.length(evaluated) == 2) prefixIfTwo else "") ++
List.join(joinDelim, List.map((test) => "(" ++ test.computeFailMsg() ++ ")", evaluated))
)
}
/**
* A matcher creator that checks the opposite of the given matcher
*
* @param matcher: a matcher to check the success of
*
* @returns a matcher that succeeds if the given matcher fails
*/
export let not = (matcher) => (value) => {
let { passed, computeFailMsg } = matcher(value)
evaluatedMatcher(!passed, () => "not (" ++ computeFailMsg() ++ ")")
}
/**
* A matcher creator function that checks if two matchers both succeed
*
* @param first: the first matcher to check the success of
* @param second: the second matcher to check the success of
*
* @returns a matcher that succeeds if both matchers succeed
*/
export let both = (first, second) => matchMultiple([first, second], true)
/**
* A matcher creator function that checks if either of two matchers succeed
*
* @param first: the first matcher to check the success of
* @param second: the second matcher to check the success of
*
* @returns a matcher that succeeds if either of the two matchers succeed
*/
export let either = (first, second) => matchMultiple([first, second], false)
/**
* A matcher creator function that checks if all of the given matchers succeed
*
* @param matchers: the list of matcher to check the success of
*
* @returns a matcher that succeeds if all of the given matchers succeed
*/
export let all = (matchers) => matchMultiple(matchers, true)
/**
* A matcher creator function that checks if any of the given matchers succeed
*
* @param matchers: the list of matcher to check the success of
*
* @returns a matcher that succeeds if any of the given matchers succeed
*/
export let any = (matchers) => matchMultiple(matchers, false)
/**
* A matcher creator creator (yes, you read that right) that defines a matcher creator based on custom matching logic
*
* @param runFn: a function to run to determine the success of the matcher;
* receives both the value being matched against and the value the returned matcher creator is invoked with.
* This function should return an AssertionInfo object
*
* @returns a matcher creator that can be given a value to match against
*/
export let binaryMatcher: ((a, b) -> AssertionInfo) -> a -> b -> AssertionInfo = (runFn) => (other) => (value) => {
runFn(value, other)
}
/**
* A matcher creator that defines a matcher based on custom matching logic
*
* @param runFn: a function to run to determine the success of the matcher;
* receives the value being matched against. This function should return an AssertionInfo object
*
* @returns a matcher creator that can be given a value to match against
*/
export let unaryMatcher: ((a) -> AssertionInfo) -> a -> AssertionInfo = (runFn) => (value) => {
runFn(value)
}
/**
* Asserts that a value fulfills a given matcher (note: does not use the native grain `assert`)
*
* @param value: a value to match against
* @param matcher: a matcher to apply to the value
*/
export let assertThat = (value, matcher) => {
match (matcher(value)) {
{ passed: true, _ } => void,
{ passed: false, computeFailMsg } => failedAssertions = ["Expected " ++ computeFailMsg(), ...failedAssertions]
}
}
/**
* Asserts that a value fulfills a given matcher, with a message to display upon failure (the message is prepended with `"Expected that "` in output).
* (note: does not use the native grain `assert`)
*
* @param message: a message to be printed in the case that the assertion fails
* @param value: a value to match against
* @param matcher: a matcher to apply to the value
*/
export let assertWithMsgThat = (message, value, matcher) => {
match (matcher(value)) {
{ passed: true, _ } => void,
{ passed: false, _ } => failedAssertions = ["Expected that " ++ message, ...failedAssertions]
}
}