bare-script 3.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,529 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+
5
+ include <args.bare>
6
+ include <dataTable.bare>
7
+ include <diff.bare>
8
+
9
+
10
+ # $function: unittestRunTest
11
+ # $group: unittest.bare
12
+ # $doc: Run a unit test
13
+ # $arg testName: The test function name
14
+ async function unittestRunTest(testName):
15
+ # Tests disabled?
16
+ if systemGlobalGet('vUnittestDisabled'):
17
+ return
18
+ endif
19
+
20
+ # Single test argument?
21
+ vUnittestTest = systemGlobalGet('vUnittestTest')
22
+ if vUnittestTest && vUnittestTest != testName:
23
+ return
24
+ endif
25
+
26
+ # Get the global unittest data
27
+ unittestData = unittestDataGet()
28
+ unittestTests = objectGet(unittestData, 'tests')
29
+ unittestWarnings = objectGet(unittestData, 'warnings')
30
+
31
+ # Test run multiple times?
32
+ if objectHas(unittestTests, testName):
33
+ arrayPush(unittestWarnings, 'Test "' + testName + '" run multiple times')
34
+ return
35
+ endif
36
+
37
+ # Get the test func
38
+ testFn = systemGlobalGet(testName)
39
+ if !testFn:
40
+ arrayPush(unittestWarnings, 'Test "' + testName + '" not found')
41
+ return
42
+ endif
43
+
44
+ # Add the unit test result array
45
+ testFailures = []
46
+ objectSet(unittestTests, testName, testFailures)
47
+
48
+ # Run the test
49
+ objectSet(unittestData, 'current', testName)
50
+ testFn()
51
+ objectSet(unittestData, 'current', null)
52
+ endfunction
53
+
54
+
55
+ # For backward compatibility
56
+ async function unittestRunTestAsync(testName):
57
+ systemLogDebug('unittest.bare: unittestRunTestAsync is deprecated - use unittestRunTest')
58
+ return unittestRunTest(testName)
59
+ endfunction
60
+
61
+
62
+ # $function: unittestEqual
63
+ # $group: unittest.bare
64
+ # $doc: Assert an actual value is equal to the expected value
65
+ # $arg actual: The actual value
66
+ # $arg expected: The expected value
67
+ # $arg description: The description of the assertion
68
+ function unittestEqual(actual, expected, description):
69
+ if actual == expected:
70
+ return
71
+ endif
72
+
73
+ # Get the global unittest data
74
+ unittestData = unittestDataGet()
75
+ unittestTests = objectGet(unittestData, 'tests')
76
+ unittestCurrent = objectGet(unittestData, 'current')
77
+ testFailures = objectGet(unittestTests, unittestCurrent)
78
+
79
+ # Add the test failure error lines
80
+ errorLines = [ \
81
+ 'Equal:', \
82
+ '', \
83
+ '```', \
84
+ jsonStringify(actual), \
85
+ '```', \
86
+ '', \
87
+ '```', \
88
+ jsonStringify(expected), \
89
+ '```' \
90
+ ]
91
+ if description:
92
+ arrayPush(testFailures, arrayExtend([markdownEscape(description), ''], errorLines))
93
+ else:
94
+ arrayPush(testFailures, errorLines)
95
+ endif
96
+ endfunction
97
+
98
+
99
+ # $function: unittestDeepEqual
100
+ # $group: unittest.bare
101
+ # $doc: Assert an actual value is *deeply* equal to the expected value
102
+ # $arg actual: The actual value
103
+ # $arg expected: The expected value
104
+ # $arg description: The description of the assertion
105
+ function unittestDeepEqual(actual, expected, description):
106
+ # Serialize as JSON
107
+ actualJSON = if(systemType(actual) == 'string', actual, jsonStringify(actual, 4))
108
+ expectedJSON = if(systemType(expected) == 'string', expected, jsonStringify(expected, 4))
109
+ if actualJSON == expectedJSON:
110
+ return
111
+ endif
112
+
113
+ # Compute the difference lines
114
+ errorLines = []
115
+ diffErrorLines = []
116
+ for diff in diffLines(actualJSON, expectedJSON):
117
+ diffType = objectGet(diff, 'type')
118
+ if diffType == 'Remove':
119
+ for diffLine in objectGet(diff, 'lines'):
120
+ arrayPush(diffErrorLines, '--- ' + diffLine)
121
+ endfor
122
+ elif diffType == 'Add':
123
+ for diffLine in objectGet(diff, 'lines'):
124
+ arrayPush(diffErrorLines, '+++ ' + diffLine)
125
+ endfor
126
+ else:
127
+ # diffType == 'Identical'
128
+ if diffErrorLines:
129
+ arrayExtend(errorLines, ['Deep-equal:', '', '```'])
130
+ arrayExtend(errorLines, diffErrorLines)
131
+ arrayPush(errorLines, '```')
132
+ diffErrorLines = []
133
+ endif
134
+ endif
135
+ endfor
136
+ if diffErrorLines:
137
+ arrayExtend(errorLines, ['Deep-equal:', '', '```'])
138
+ arrayExtend(errorLines, diffErrorLines)
139
+ arrayPush(errorLines, '```')
140
+ endif
141
+
142
+ # Get the global unittest data
143
+ unittestData = unittestDataGet()
144
+ unittestTests = objectGet(unittestData, 'tests')
145
+ unittestCurrent = objectGet(unittestData, 'current')
146
+ testFailures = objectGet(unittestTests, unittestCurrent)
147
+
148
+ # Add the test failure error lines
149
+ if description:
150
+ arrayPush(testFailures, arrayExtend([markdownEscape(description), ''], errorLines))
151
+ else:
152
+ arrayPush(testFailures, errorLines)
153
+ endif
154
+ endfunction
155
+
156
+
157
+ # Helper to get the global unittest data object
158
+ function unittestDataGet():
159
+ unittestData = systemGlobalGet('unittestData')
160
+ if unittestData == null:
161
+ unittestData = {'tests': {}, 'warnings': [], 'current': null}
162
+ systemGlobalSet('unittestData', unittestData)
163
+ endif
164
+ return unittestData
165
+ endfunction
166
+
167
+
168
+ # $function: unittestReport
169
+ # $group: unittest.bare
170
+ # $doc: Render the unit test report
171
+ # $arg options: Optional unittest report options object. The following options are available:
172
+ # $arg options: - **coverageExclude** - array of script names to exclude from coverage
173
+ # $arg options: - **coverageMin** - verify minimum coverage percent (0 - 100)
174
+ # $arg options: - **links** - the array of page links
175
+ # $arg options: - **title** - the page title
176
+ # $return: The number of unit test failures
177
+ function unittestReport(options):
178
+ coverageMin = if(options, objectGet(options, 'coverageMin', 0), 0)
179
+ coverageExclude = if(options, objectGet(options, 'coverageExclude'))
180
+ links = if(options, objectGet(options, 'links'))
181
+ defaultTitle = 'unittestReport'
182
+ title = if(options, objectGet(options, 'title', defaultTitle), defaultTitle)
183
+
184
+ # Parse arguments
185
+ args = argsParse(unittestReportArguments)
186
+ isDisabled = objectGet(args, 'disabled')
187
+ hideTests = objectGet(args, 'hideTests')
188
+ isReport = objectGet(args, 'report')
189
+ scriptName = objectGet(args, 'script')
190
+ testNameArg = objectGet(args, 'test')
191
+
192
+ # Disabled?
193
+ if isDisabled:
194
+ return 0
195
+ endif
196
+
197
+ # Script coverage details page?
198
+ if scriptName:
199
+ unittestReportScript(scriptName)
200
+ return 0
201
+ endif
202
+
203
+ # Get the global unittest data
204
+ unittestData = unittestDataGet()
205
+ unittestTests = objectGet(unittestData, 'tests')
206
+ unittestWarnings = objectGet(unittestData, 'warnings')
207
+
208
+ # Compute test statistics
209
+ testNames = arraySort(objectKeys(unittestTests))
210
+ testCount = arrayLength(testNames)
211
+ testFailCount = 0
212
+ for testName in testNames:
213
+ testFailures = objectGet(unittestTests, testName)
214
+ if arrayLength(testFailures):
215
+ testFailCount = testFailCount + 1
216
+ endif
217
+ endfor
218
+ testPassCount = testCount - testFailCount
219
+
220
+ # Render the links
221
+ if !isReport && links:
222
+ markdownPrint(arrayJoin(links, ' | '))
223
+ endif
224
+
225
+ # Render the page title
226
+ documentSetTitle(title)
227
+ markdownPrint( \
228
+ '# ' + markdownEscape(title), \
229
+ '', \
230
+ 'Ran ' + testCount + ' tests - ' + testPassCount + ' passed, ' + testFailCount + ' failed' \
231
+ )
232
+ if !isReport:
233
+ if testNameArg:
234
+ markdownPrint('', argsLink(unittestReportArguments, 'All tests', null, true))
235
+ else:
236
+ markdownPrint('', argsLink(unittestReportArguments, if(hideTests, 'Show', 'Hide') + ' tests', {'hideTests': !hideTests}))
237
+ endif
238
+ endif
239
+
240
+ # Report any warnings
241
+ testWarningCount = arrayLength(unittestWarnings)
242
+ if testWarningCount:
243
+ markdownPrint('', '## Warnings')
244
+ for warning in unittestWarnings:
245
+ markdownPrint('', '- ' + markdownEscape(warning))
246
+ endfor
247
+ endif
248
+
249
+ # Report the failing tests
250
+ if testFailCount:
251
+ markdownPrint('', '## Failing Tests')
252
+ if hideTests && !isReport && !testNameArg:
253
+ markdownPrint('', argsLink(unittestReportArguments, 'Show', {'hideTests': false}))
254
+ endif
255
+ if !hideTests || testNameArg:
256
+ for testName in testNames:
257
+ testFailures = objectGet(unittestTests, testName)
258
+ if arrayLength(testFailures):
259
+ failureLink = if(isReport, testName, \
260
+ argsLink(unittestReportArguments, testName, {'test': testName}, false, '_top'))
261
+ failureLines = ['', failureLink + ' - FAIL']
262
+ for errorLines in testFailures:
263
+ for errorLine, ixErrorLine in errorLines:
264
+ if ixErrorLine == 0:
265
+ arrayPush(failureLines, '')
266
+ arrayPush(failureLines, '- ' + errorLine)
267
+ else:
268
+ arrayPush(failureLines, if(errorLine, ' ' + errorLine, ''))
269
+ endif
270
+ endfor
271
+ endfor
272
+ markdownPrint(failureLines)
273
+ endif
274
+ endfor
275
+ endif
276
+ endif
277
+
278
+ # Report the passing tests
279
+ if testPassCount:
280
+ markdownPrint('', '## Passing Tests')
281
+ if hideTests && !isReport && !testNameArg:
282
+ markdownPrint('', argsLink(unittestReportArguments, 'Show', {'hideTests': false}))
283
+ endif
284
+ if !hideTests || testNameArg:
285
+ for testName in testNames:
286
+ testFailures = objectGet(unittestTests, testName)
287
+ if !arrayLength(testFailures):
288
+ testLink = if(isReport, testName, \
289
+ argsLink(unittestReportArguments, testName, {'test': testName}, false, '_top'))
290
+ markdownPrint('', testLink + ' - OK')
291
+ endif
292
+ endfor
293
+ endif
294
+ endif
295
+
296
+ # Coverage report
297
+ coverage = coverageGlobalGet()
298
+ coverageFailCount = 0
299
+ if !testNameArg && coverage:
300
+ markdownPrint('', '## Coverage Report')
301
+
302
+ # Compute the coverage data table
303
+ coverageData = unittestCoverageData(coverageExclude)
304
+ if coverageData:
305
+ # Verify the coverage percent
306
+ coveragePercent = objectGet(arrayGet(coverageData, arrayLength(coverageData) - 1), 'Coverage')
307
+ if coveragePercent < coverageMin:
308
+ markdownPrint( \
309
+ '', \
310
+ '**Error**: Coverage percentage, ' + numberToFixed(coveragePercent, 1, true) + \
311
+ '%, is below the minimum coverage percentage of ' + numberToFixed(coverageMin, 1, true) + '%.' \
312
+ )
313
+ coverageFailCount = coverageFailCount + 1
314
+ endif
315
+
316
+ # Render the coverage data table
317
+ for row in coverageData:
318
+ scriptName = objectGet(row, 'Script')
319
+ if !isReport && !stringStartsWith(scriptName, '**'):
320
+ objectSet(row, 'Script', argsLink(unittestReportArguments, scriptName, {"script": scriptName}, false, "_top"))
321
+ endif
322
+ objectSet(row, 'Coverage', numberToFixed(objectGet(row, 'Coverage'), 1) + '%')
323
+ endfor
324
+ markdownPrint('', dataTableMarkdown(coverageData, { \
325
+ 'fields': ['Script', 'Statements', 'Missing', 'Coverage'], \
326
+ 'formats': { \
327
+ 'Statements': {'align': 'right'}, \
328
+ 'Missing': {'align': 'right'}, \
329
+ 'Coverage': {'align': 'right'} \
330
+ } \
331
+ }))
332
+ else:
333
+ markdownPrint('', '*No data.*')
334
+ endif
335
+ endif
336
+
337
+ # Return status code
338
+ return testWarningCount + testFailCount + coverageFailCount
339
+ endfunction
340
+
341
+
342
+ # unittestReport application arguments
343
+ unittestReportArguments = argsValidate([ \
344
+ {'name': 'disabled', 'global': 'vUnittestDisabled', 'type': 'bool', 'default': false}, \
345
+ {'name': 'hideTests', 'global': 'vUnittestHideTests', 'type': 'bool', 'default': false}, \
346
+ {'name': 'report', 'global': 'vUnittestReport', 'type': 'bool', 'default': false}, \
347
+ {'name': 'script', 'global': 'vUnittestScript'}, \
348
+ {'name': 'test', 'global': 'vUnittestTest'} \
349
+ ])
350
+
351
+
352
+ # Render the script coverage details page
353
+ function unittestReportScript(scriptName):
354
+ # Render the script coverage page title
355
+ title = scriptName + ' Coverage'
356
+ documentSetTitle(title)
357
+ markdownPrint( \
358
+ argsLink(unittestReportArguments, 'Back', {'script': null}, false, '_top'), \
359
+ '', \
360
+ '# ' + markdownEscape(title) \
361
+ )
362
+
363
+ # Get the script coverage data
364
+ coverage = coverageGlobalGet()
365
+ scripts = if(coverage, objectGet(coverage, 'scripts'))
366
+ coverageScript = if(scripts, objectGet(scripts, scriptName))
367
+ if !coverageScript:
368
+ markdownPrint('', '*No data.*')
369
+ return
370
+ endif
371
+ script = objectGet(coverageScript, 'script')
372
+ covered = objectGet(coverageScript, 'covered')
373
+ scriptLines = objectGet(script, 'scriptLines')
374
+
375
+ # Mark the statements covered or not-covered
376
+ lineColors = {}
377
+ statements = unittestScriptStatements(script)
378
+ for statement in statements:
379
+ statementKey = arrayGet(objectKeys(statement), 0)
380
+ lineNumber = objectGet(objectGet(statement, statementKey), 'lineNumber')
381
+ lineCount = objectGet(objectGet(statement, statementKey), 'lineCount', 1)
382
+ lineStr = stringNew(lineNumber)
383
+ lineColor = if(objectHas(covered, lineStr), '#a0a0a030', '#f0808080')
384
+ objectSet(lineColors, lineStr, lineColor)
385
+ if lineCount > 1:
386
+ lineNumberEnd = lineNumber + lineCount
387
+ lineNumber = lineNumber + 1
388
+ while lineNumber < lineNumberEnd:
389
+ objectSet(lineColors, stringNew(lineNumber), lineColor)
390
+ lineNumber = lineNumber + 1
391
+ endwhile
392
+ endif
393
+ endfor
394
+
395
+ # Generate the code coverage details
396
+ codeLineElements = []
397
+ codeElements = {'html': 'pre', 'elem': codeLineElements}
398
+ currentLines = []
399
+ currentColor = null
400
+ for line, ixLine in scriptLines:
401
+ # Accumulate lines of the same color
402
+ lineColor = objectGet(lineColors, stringNew(ixLine + 1))
403
+ if lineColor == currentColor:
404
+ arrayPush(currentLines, line)
405
+ continue
406
+ endif
407
+
408
+ # Render the current lines
409
+ unittestReportScriptLines(codeLineElements, currentLines, currentColor)
410
+ currentLines = [line]
411
+ currentColor = lineColor
412
+ endfor
413
+ unittestReportScriptLines(codeLineElements, currentLines, currentColor, true)
414
+ elementModelRender(codeElements)
415
+ endfunction
416
+
417
+
418
+ function unittestReportScriptLines(codeLineElements, lines, color, noNewline):
419
+ if lines:
420
+ linesStr = arrayJoin(lines, '\n') + if(noNewline, '', '\n')
421
+ if !color:
422
+ arrayPush(codeLineElements, {'text': linesStr})
423
+ else:
424
+ arrayPush(codeLineElements, { \
425
+ 'html': 'span', \
426
+ 'attr': {'style': 'display: block; background-color: ' + color}, \
427
+ 'elem': {'text': linesStr} \
428
+ })
429
+ endif
430
+ endif
431
+ endfunction
432
+
433
+
434
+ # Get the coverage data table - columns are "Script", "Statement", "Covered", and "Percent"
435
+ function unittestCoverageData(coverageExclude):
436
+ data = []
437
+
438
+ # Get the global coverage object
439
+ coverage = coverageGlobalGet()
440
+ if coverage:
441
+ # Get the script names with coverage data
442
+ scripts = objectGet(coverage, 'scripts')
443
+ scriptNames = if(scripts, arraySort(objectKeys(scripts)), [])
444
+
445
+ # Compute script statement coverage
446
+ totalStatements = 0
447
+ totalMissing = 0
448
+ for scriptName in scriptNames:
449
+ # Excluded?
450
+ if coverageExclude:
451
+ isExcluded = false
452
+ for excludeScriptName in coverageExclude:
453
+ if stringEndsWith(scriptName, excludeScriptName):
454
+ isExcluded = true
455
+ break
456
+ endif
457
+ endfor
458
+ if isExcluded:
459
+ continue
460
+ endif
461
+ endif
462
+
463
+ # Get the script members
464
+ coverageScript = objectGet(scripts, scriptName)
465
+ script = objectGet(coverageScript, 'script')
466
+ covered = objectGet(coverageScript, 'covered')
467
+ statements = unittestScriptStatements(script)
468
+ statementCount = arrayLength(statements)
469
+
470
+ # Count statements with coverage
471
+ missingCount = 0
472
+ for statement in statements:
473
+ statementKey = arrayGet(objectKeys(statement), 0)
474
+ lineNumber = objectGet(objectGet(statement, statementKey), 'lineNumber')
475
+ lineStr = stringNew(lineNumber)
476
+ if !objectHas(covered, lineStr):
477
+ missingCount = missingCount + 1
478
+ totalMissing = totalMissing + 1
479
+ endif
480
+ totalStatements = totalStatements + 1
481
+ endfor
482
+
483
+ # Add the script coverage data row
484
+ coveragePercent = if(statementCount, 100 * (statementCount - missingCount) / statementCount, 100)
485
+ arrayPush(data, { \
486
+ 'Script': scriptName, \
487
+ 'Statements': statementCount, \
488
+ 'Missing': missingCount, \
489
+ 'Coverage': coveragePercent, \
490
+ 'CoverageStr': numberToFixed(coveragePercent, 1) + '%' \
491
+ })
492
+ endfor
493
+
494
+ # Add the coverage totals data row
495
+ if scriptNames:
496
+ coveragePercent = if(totalStatements, 100 * (totalStatements - totalMissing) / totalStatements, 100)
497
+ arrayPush(data, { \
498
+ 'Script': '**Total**', \
499
+ 'Statements': totalStatements, \
500
+ 'Missing': totalMissing, \
501
+ 'Coverage': coveragePercent, \
502
+ 'CoverageStr': numberToFixed(coveragePercent, 1) + '%' \
503
+ })
504
+ endif
505
+ endif
506
+
507
+ return data
508
+ endfunction
509
+
510
+
511
+ # Get a script's array of statements
512
+ function unittestScriptStatements(script):
513
+ statements = []
514
+
515
+ # Add the top-level script statements
516
+ for statement in objectGet(script, 'statements'):
517
+ arrayPush(statements, statement)
518
+
519
+ # Add function statements
520
+ statementKey = arrayGet(objectKeys(statement), 0)
521
+ if statementKey == 'function':
522
+ for funcStatement in objectGet(objectGet(statement, statementKey), 'statements'):
523
+ arrayPush(statements, funcStatement)
524
+ endfor
525
+ endif
526
+ endfor
527
+
528
+ return statements
529
+ endfunction