-
Notifications
You must be signed in to change notification settings - Fork 0
/
cbUnit.cb
895 lines (760 loc) · 38.8 KB
/
cbUnit.cb
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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
// Libraries and other includes
Include "Libraries\Includes.cb"
Include "CompilerErrors.cb"
// Constants and globals
Const CBUNIT_VERSION = "0.0.0"
Const CBUNIT_TESTS_DIRECTORY = "tests"
Const CBUNIT_TEST_FILE_PATTERN = "test_*.cb"
Const CBUNIT_TEST_PROGRAM_FILE_NAME = "Editor.out" // Our final test program will be written to this file. CBCompiler.exe expects exactly this file name.
Const CBUNIT_TEST_PROGRAM_OUTPUT_FILE_NAME = "cbUnit.test.dat" // Test program writes its messages here so that cbUnit can read them and generate a report.
Const CBUNIT_REPORT_FILE_NAME = "cbUnit.report.txt" // Will contain a human readable result of the tests.
Const CBUNIT_TEST_FUNCTION_PATTERN = "Function test_*(*"
Const CBUNIT_HOOK_FUNCTION_PATTERN = "Function hook_*(*"
Const CBUNIT_HOOK_FUNCTION_NAMES = "hook_Setup:0 hook_SetupTest:1 hook_Cleanup:0 hook_CleanupTest:1" // Number :x at the end of each function name tells the number of parameters the function takes. Needed when defining empty functions. Note that hook_CrashPlan is not defined here, because its definition is not mandatory.
Const CBUNIT_CONSTANT_PATTERN = "Const CBUNIT_*=*"
Const CBUNIT_TEST_MAIN_SKELETON = "test-main.skeleton.cb"
Const CBUNIT_TEST_CALL_SKELETON = "test-call.skeleton.cb"
Const CBUNIT_MESSAGE_CONTEXT = 1
Const CBUNIT_MESSAGE_FAILURE = 2
Const CBUNIT_MESSAGE_SUCCESS = 3
Const CBUNIT_MESSAGE_FINISH = 4
Global cbUnit_StartingDirectory$ : cbUnit_StartingDirectory = CurrentDir() // Has trailing slash.
Global cbUnit_ApplicationDirectory$ : cbUnit_ApplicationDirectory = CleanPath(cbUnit_StartingDirectory) // No trailing slash. Directory where the testable application lives. If cbUnit was started from commandline or via a cmd file, CurrentDir() contains a path to where the call originated.
Global cbUnit_CompilerDirectory$ : cbUnit_CompilerDirectory = cbUnit_Directory(True)+"CBCompiler" // No trailing slash.
Global cbUnit_EOL$ : cbUnit_EOL = Chr(13) + Chr(10)
Global cbUnit_Quote$ : cbUnit_Quote = Chr(34)
// Types
Type cbUnit_TestFile
Field id
Field path$
EndType
Type cbUnit_TestFunction
Field id
Field function_name$
EndType
Type cbUnit_HookFunction
Field id
Field function_name$
EndType
Type cbUnit_Setting
Field id
Field name$
Field value$
Field source$
EndType
// Constains messages read from a file written by a test application.
Type cbUnit_Message
Field message_type
Field message_context$
Field message_content$
EndType
// Main program
Function MainProgram()
Dim test_path$, test_paths$, initialization_directory$
Dim nonoption_argument.CommandLineNonOptionArgument
DefineCommandLineOptions("--init", True, True) // First True: Allow non-option arguments. This is needed To be able To get test files/directories As parameters. Second True: enable -- command Line option that can be used To define the the rest of the command Line String should be Read As a single argument. We will use this To pass an optional command Line String To the testable application.
If Not ParseCommandLineOptions() Then HandleError("MainProgram", "Syntax error in commandl"+"ine options.")
// Interpret CommandLine options
If 0 = CountCommandLineOptionsPresent Then
// No CommandLine _options_ are defined, but there might be some non-option arguments available (but Not always).
// Check If test paths are provided in non-option arguments
If CountCommandLineNonOptionArgumentsPresent Then
For nonoption_argument = Each CommandLineNonOptionArgument
test_path = nonoption_argument\argument
If Not IsAbsolutePath(test_path) Then
test_path = cbUnit_ApplicationDirectory + "\" + test_path
EndIf
If test_paths Then test_paths + "|"
test_paths + test_path
Next nonoption_argument
Else
// Define a Default test directory
test_paths = cbUnit_ApplicationDirectory + "\" + CBUNIT_TESTS_DIRECTORY
EndIf
// Run the tests
RunTests(test_paths)
ElseIf isCommandLineOptionPresent("--init") Then
// Initialize unit tests in an application directory. This means To create a "tests" folder And a "cbUnit.cmd" file.
// Check If the application directory is defined in non-option arguments.
If CountCommandLineNonOptionArgumentsPresent Then
nonoption_argument = First(CommandLineNonOptionArgument)
initialization_directory = nonoption_argument\argument
Else
initialization_directory = cbUnit_ApplicationDirectory
EndIf
MakeError InitializeUnitTestsInDirectory(initialization_directory) // MakeError: Display the message InitializeUnitTestsInDirectory() gives us.
Else
HandleError("MainProgram", "Unrecognised commandl"+"ine options.")
EndIf
EndFunction
Function RunTests(test_paths$)
Dim report_file_path$, report_file, output_file_path$, export_test_program_path$, pass_command_line$, compiler_error$, test_program_result, crashed_function_name$, crash_plan_compiler_error$
Dim test_file.cbUnit_TestFile, message.cbUnit_Message
SCREEN TextWidth("Compiling a"+"nd running tests..."), TextHeight("H")
ClsColor 212,208,200
Color 0,0,0
Cls
DrawScreen
SetWindow "cbUnit v. " + CBUNIT_VERSION
GenerateListOfTestFiles(test_paths)
// Reset report file
report_file_path = cbUnit_Directory(True) + CBUNIT_REPORT_FILE_NAME
report_file = OpenToWrite(report_file_path)
WriteLine report_file, "cbUnit test report " + Date() + " " + Time() + ":"
// Test Each test file
For test_file = Each cbUnit_TestFile
// Reset test output file
output_file_path = cbUnit_Directory(True) + CBUNIT_TEST_PROGRAM_OUTPUT_FILE_NAME
TruncateFile(output_file_path)
// Scaffold test program
GenerateListOfTestFunctionsAndConstants(test_file\path) // ScaffoldTestProgram() needs this
ScaffoldTestProgram(test_file\path, output_file_path, cbUnit_ApplicationDirectory)
// Export the test program If requested
If Int(getSetting("EXPORT_TEST_PROGRAM")) Then // Int() needed so that "0" would Not be interpreted As True.
// CBUNIT_EXPORT_TEST_PROGRAM setting is 1 Or 2. Both means To export, but 2 means To open the exported file too.
export_test_program_path = FilePathInfo(test_file\path,"dirname") + "\exported_" + FilePathInfo(test_file\path,"basename") // For example C:\CoolBasic\Projects\MyApplication\tests\test_MyApplication.cb becomes C:\CoolBasic\Projects\MyApplication\tests\exported_test_MyApplication.cb
CopyFile cbUnit_CompilerDirectory+"\"+CBUNIT_TEST_PROGRAM_FILE_NAME, export_test_program_path // The source file Is generated in ScaffoldTestProgram().
If 2 = getSetting("EXPORT_TEST_PROGRAM") Then Execute export_test_program_path
EndIf
// Get possible command Line arguments that should be passed To the test program
// 1. Try To get the from cbUnit's command Line.
// 2. If command Line did Not have them, try To get them from the test_*.cb condifuration constants.
// 3. If still Not found, we can use an empty String, meaning no command Line arguments are used.
pass_command_line = getCommandLineArgument("--", getSetting("CommandLine", ""))
// Compile and run the test program
compiler_error = CompileAndRun(pass_command_line)
// Write report
test_program_result = GenerateAndWriteReport("normal", report_file, output_file_path, test_file\path, compiler_error)
// Check the result To see If the test program crashed
Select test_program_result
Case 0
// Nothing unusual. Assertions may have succeeded Or failed, but there's nothing To react To here.
Case 1
// The test program has crashed.
// Check If the program contains a hook_CrashPlan() Function.
If isHookFunctionDefined("hook_CrashPlan") Then
// A crash plan Function is defined.
// Compile a program that calls the crashplan Function
WriteLine report_file, " hook_CrashPlan() f"+"unction is defined. Result of running the crash plan program:"
message = Last(cbUnit_Message)
crashed_function_name = message\message_context // This indicates the Last Function that was called Before the program crashed.
TruncateFile(output_file_path)
ScaffoldCrashPlanProgram(test_file\path, output_file_path, cbUnit_ApplicationDirectory, crashed_function_name)
crash_plan_compiler_error = CompileAndRun(pass_command_line)
GenerateAndWriteReport("crash_plan", report_file, output_file_path, test_file\path, crash_plan_compiler_error)
Else
// No crashplan Function is defined.
WriteLine report_file, " Will n"+"ot run a crash plan program because a f"+"unction named hook_CrashPlan() is n"+"ot defined."
WriteLine report_file, ""
EndIf
EndSelect
Next test_file
// Close the report file
CloseFile report_file
// Open the report file and end the program
Execute(report_file_path)
End
EndFunction
Function cbUnit_Directory$(end_with_backslash=0)
Dim path$
// Check If cbUnit is currently run from CBEditor
If Lower(Right(GetEXEName(), 9)) = "cbrun.exe" Then
// cbUnit is currently run from CBEditor, so we cannot use GetEXEName() To determine the application directory because that would lead To the editor's IDE\ folder.
// We can use cbUnit_StartingDirectory (= i.e. value of CurrentDir() at the very beginning of this program) because when compiling from CBEditor, CurrentDir() is rarely changed To point anywhere Else than To the application's directory.
path = cbUnit_StartingDirectory
Else
// This is a standalone executable program. Determine the program's directory from GetEXEName(), Not from cbUnit_StartingDirectory, because that may point To a testable host application.
path = GetEXEName() // Contains a directory And executable name
path + "\.." // Trick CleanPath() To remove the executable name from the path.
EndIf
Return CleanPath(path, end_with_backslash)
EndFunction
// Compiling
Function CompileAndRun(command_line_parameters$)
Dim force_variable_declaration, instructions, cmd, signal_file_path$, compiler_log_path$
// Write instructions For CBCompiler
force_variable_declaration = getSetting("FORCE_VARIABLE_DECLARATION")
instructions = OpenToWrite(cbUnit_Directory(True)+"CBCompiler\Compiler")
WriteLine instructions, "t"+"ype=1" // 1: We want To build an exe. 0 would mean that we would only want To check syntax.
WriteLine instructions, "sourcedir="+cbUnit_ApplicationDirectory+"\" // Application root directory. Include commands in the test_*.cb files rely ON this command.
WriteLine instructions, "buildto=cbRun" // changing cbRun To something Else does Not work, the compiler will create a cbRun.exe file any way.
WriteLine instructions, "force=" + force_variable_declaration // 1: turn Force Variable Declaraction ON. 0: turn it OFF.
CloseFile instructions
// Create a .cmd file that will call the compiler, Wait that it finishes, and then call the test program And Wait that it finishes, And Then Delete a signal file.
cmd = OpenToWrite(cbUnit_CompilerDirectory+"\call_compiler.cmd")
WriteLine cmd, "@echo OFF"
WriteLine cmd, "echo Preparations..."
WriteLine cmd, "cd " + cbUnit_Quote + cbUnit_Directory(True)+"CBCompiler" + cbUnit_Quote
WriteLine cmd, "del cbRun.exe" // Should Not exist at this point but try To Delete just in Case
WriteLine cmd, "del cbUnitTestProgram.exe" // Should Not exist at this point but try To Delete just in Case
WriteLine cmd, "echo Compiling..."
WriteLine cmd, "call CBCompiler.exe"
WriteLine cmd, "If exist cbRun.exe ("
WriteLine cmd, " echo Running testing application..."
WriteLine cmd, " ren cbRun.exe cbUnitTestProgram.exe"
WriteLine cmd, " call cbUnitTestProgram.exe " + command_line_parameters
WriteLine cmd, " echo Finishing..."
WriteLine cmd, " del CompileLog.txt" // Only Delete CompileLog.txt If compiling was successful.
WriteLine cmd, ") Else ("
WriteLine cmd, " echo Compiling failed!"
WriteLine cmd, " echo Cleaning up..."
WriteLine cmd, ")"
WriteLine cmd, "del cbUnitTestProgram.exe"
WriteLine cmd, "del signal"
WriteLine cmd, "del Compiler" // Delete the instructions file
WriteLine cmd, "del Editor.out"
WriteLine cmd, "del %0" // Delete call_compiler.cmd
CloseFile cmd
// Signal ourself that the test program is running (actually, will soon be running)
signal_file_path = cbUnit_Directory(True)+"CBCompiler\signal"
CloseFile OpenToWrite(signal_file_path) // The compiler does Not use this file at all
// Now we can actually compile & run the program
Execute cbUnit_Directory(True)+"CBCompiler\call_compiler.cmd"
// Wait Until the program execution is finished
While FileExists(signal_file_path)
Text 0,0, "Compiling a"+"nd running tests..."
DrawScreen
Wait(100)
// TODO: Break out If the signal file is stuck Forever.
Wend
// The program has finished running
compiler_log_path = cbUnit_Directory(True)+"CBCompiler\CompileLog.txt"
If FileExists(compiler_log_path) Then
// CompileLog.txt was Not deleted by call_compiler.cmd, which usually means that the compiling has failed.
// Check did the compiling really fail And If so, Return the compile error.
Return InspectCompileLog(compiler_log_path, True)
EndIf
Return "" // Means: no compile error
EndFunction
Function InspectCompileLog(compiler_log_path$, delete_log=0)
Dim compiler_log, header$, line_$, subject$, value$, error_code, error_module$, error_line, error_detail$, error_source_code$, indent$, result$
// Force Variable Declaration example of CompileLog.txt (no indentation):
// ERROR
// code 37
// module c:\path\To\cbunit\cbunit.runtime.cb
// Line 11
// detail name_of_undefined_variable
// Another example with an error "Cannot process due To irregular String marks at Line 7":
// ERROR
// code 10
// Line 7
// detail
compiler_log = OpenToRead(compiler_log_path)
header = Upper(ReadLine(compiler_log))
If header = "ERROR" Then
// Some compilation error has happened
// Read the Log content into error_* variables
While Not EOF(compiler_log)
line_ = Trim(ReadLine(compiler_log))
subject = GetWord2(line_, 1, " ") // E.g. "code", Or "module"
subject = Upper(Left(subject,1)) + Lower(Mid(subject,2)) // Capitalize subject so that it fits our Select Case strutuce below. This is due To CBEditor making the literal String "Line" capitalised.
value = Trim(Mid(line_, Len(subject)+1)) // E.g. "37" Or "c:\path\To\cbunit\cbunit.runtime.cb"
Select subject
Case "Code"
error_code = Int(value)
// Ensure that code is between 0 - 200. Should Not be anything Else, but do this just in Case To avoid irregular Read of an array.
If error_code > 200 Or error_code < 0 Then error_code = 0 // Error code 0 does Not exist, but it's ok, it will result in an empty String in the error message.
Case "Module"
// Path To a *.cb file that contains the error. This is Not always present in CompileLog.txt For some reason.
error_module = value
Case "Line"
error_line = Int(value)
Case "Detail"
// This is usually some variable name Or something similar
error_detail = value
EndSelect
Wend
// If we have both error_module And error_line, "sneak peak" the problematic source code Line so that it can be added To the result message.
error_source_code = ""
If error_module And error_line Then
error_source_code = ReadLineFromFile(error_module, error_line, "", "")
If error_detail Then
// Check that CBCompiler has given us a correct file path by comparing the error detail (If we happen To have one) To the problematic source code Line.
If Not PatternCompare("*"+error_detail+"*", error_source_code, False) Then
// CBCompiler has probably indicated a wrong source code file.
error_source_code = "CBCompiler reported a wrong source code file! <"
// TODO: We could also try If we can find the problem from the current test_*.cb file. But that would Not cover cases where the syntax error is located in the application code.
EndIf
EndIf
EndIf
// Generate a human readable message
indent = " "
result = indent + "Compile error "+error_code+": " + getCompilerError(error_code, error_detail) + cbUnit_EOL
If error_line Then
result = result + indent + "At l"+"ine " + error_line // No newline here
If error_module Then result = result + " in " + error_module // No newline here
result + cbUnit_EOL
ElseIf error_module
result = result + indent + "in " + error_module + cbUnit_EOL
EndIf
If error_source_code Then result = result + indent + " > " + error_source_code + cbUnit_EOL // A preview of the problematic source code Line.
Else
// Compilation was successful
// (We do Not come here After every successful compilation; this function is called only If CompileLog.txt was Not deleted. But it should be deleted After a successful compilation.)
result = ""
EndIf
CloseFile compiler_log
If delete_log Then DeleteFile compiler_log_path
Return result
EndFunction
// Generators / Scaffolders
Function ScaffoldTestProgram(test_file_path$, output_file_path$, application_directory$)
Dim call_skeleton_file, call_skeleton$, test_program_file, runtime_setting_definitions$, hook_function_definions$, main_skeleton_file, line_$, call$
Dim test_function.cbUnit_TestFunction
// Prepare a helper skeleton For test function calls (we will modify it later)
call_skeleton_file = OpenToRead(cbUnit_Directory(True)+CBUNIT_TEST_CALL_SKELETON)
call_skeleton$ = ""
While Not EOF(call_skeleton_file)
call_skeleton = call_skeleton + ReadLine(call_skeleton_file) + cbUnit_EOL
Wend
CloseFile call_skeleton_file
// Prepare Write destination
test_program_file = OpenToWrite(cbUnit_CompilerDirectory+"\"+CBUNIT_TEST_PROGRAM_FILE_NAME)
// Write runtime settings that are Not present in the test_*.cb file
runtime_setting_definitions = EnsureRuntimeSettingsAreDefined()
If runtime_setting_definitions Then
WriteLine test_program_file, "// Define runtime settings with d"+"efault values:"
WriteLine test_program_file, runtime_setting_definitions
WriteLine test_program_file, ""
EndIf
// Write empty hook functions To substitute those that are Not present in the test_*.cb file
hook_function_definions = EnsureHookFunctionsAreDefined()
If hook_function_definions Then
WriteLine test_program_file, "// Define empty hook functions:"
WriteLine test_program_file, hook_function_definions
WriteLine test_program_file, ""
EndIf
// Main skeleton
main_skeleton_file = OpenToRead(cbUnit_Directory(True)+CBUNIT_TEST_MAIN_SKELETON)
While Not EOF(main_skeleton_file)
line_$ = ReadLine(main_skeleton_file)
If "// INSERT TEST CALLS HERE //" = line_ Then
// This is a mark that we should Insert test Function calls here
For test_function = Each cbUnit_TestFunction
call$ = call_skeleton
call = IReplace(call, "@test_function_name", test_function\function_name)
WriteLine test_program_file, call
Next test_function
Else
// Normal code Line. Substitute variables.
line_ = IReplace(line_, "@test_file_path", test_file_path)
line_ = IReplace(line_, "@output_file_path", output_file_path)
line_ = IReplace(line_, "@application_directory", application_directory)
line_ = IReplace(line_, "@cbunit_directory", cbUnit_Directory())
line_ = IReplace(line_, "@is_running_crash_plan", False)
WriteLine test_program_file, line_
EndIf
Wend
CloseFile main_skeleton_file
CloseFile test_program_file
EndFunction
Function ScaffoldCrashPlanProgram(test_file_path$, output_file_path$, application_directory$, crashed_function_name$)
Dim crash_plan_program_file, runtime_setting_definitions$, hook_function_definions$, main_skeleton_file, line_$, call$
// Prepare Write destination
crash_plan_program_file = OpenToWrite(cbUnit_CompilerDirectory+"\"+CBUNIT_TEST_PROGRAM_FILE_NAME) // We can use the CBUNIT_TEST_PROGRAM_FILE_NAME constant For a crash plan file too, because the value is actually more generic than just For _test_ prgorams. It's "Editor.out".
// Write runtime settings that are Not present in the test_*.cb file
// cbUnit's internal functions should Not need these when running a crash plan, but it's theoretically possible that the constants are used by the application developer in the test_*.cb file.
runtime_setting_definitions = EnsureRuntimeSettingsAreDefined()
If runtime_setting_definitions Then
WriteLine crash_plan_program_file, "// Define runtime settings with d"+"efault values:"
WriteLine crash_plan_program_file, runtime_setting_definitions
WriteLine crash_plan_program_file, ""
EndIf
// Write empty hook functions To substitute those that are Not present in the test_*.cb file
// Needed because tst-main.skeleton.cb has a call For hook_Setup() Function. It's Not actually called when running a crash plan, but the call symbol still exists in the program, so the definition must exist too.
hook_function_definions = EnsureHookFunctionsAreDefined()
If hook_function_definions Then
WriteLine crash_plan_program_file, "// Define empty hook functions:"
WriteLine crash_plan_program_file, hook_function_definions
WriteLine crash_plan_program_file, ""
EndIf
// Main skeleton
main_skeleton_file = OpenToRead(cbUnit_Directory(True)+CBUNIT_TEST_MAIN_SKELETON)
While Not EOF(main_skeleton_file)
line_$ = ReadLine(main_skeleton_file)
If "// INSERT TEST CALLS HERE //" = line_ Then
// This is a mark that we should Insert the crash plan Function call here (although the comment says "TEST CALLS", we can use it For the crash plan Function call in this Case).
WriteLine crash_plan_program_file, "cbUnit_SetCurrentContext("+Chr(34)+"hook_CrashPlan"+Chr(34)+")"
WriteLine crash_plan_program_file, "hook_CrashPlan("+Chr(34)+crashed_function_name+Chr(34)+")"
WriteLine crash_plan_program_file, "cbUnit_SetCurrentContext("+Chr(34)+Chr(34)+")"
Else
// Normal code Line. Substitute variables.
line_ = IReplace(line_, "@test_file_path", test_file_path)
line_ = IReplace(line_, "@output_file_path", output_file_path)
line_ = IReplace(line_, "@application_directory", application_directory)
line_ = IReplace(line_, "@cbunit_directory", cbUnit_Directory())
line_ = IReplace(line_, "@is_running_crash_plan", True)
WriteLine crash_plan_program_file, line_
EndIf
Wend
CloseFile main_skeleton_file
CloseFile crash_plan_program_file
EndFunction
// Returns a string of constants that are not defined in the current test_*.cb file.
Function EnsureRuntimeSettingsAreDefined()
Dim setting_definitions$, runtime_settings$, i, runtime_setting$, runtime_setting_key$, runtime_setting_value$, runtime_setting_default_value$
Dim setting.cbUnit_Setting
// Define settings And Default values. Separate settings from each other by commas, And setting keys And values by equal sign.
runtime_settings = "STOP_AT="
setting_definitions = ""
For i = 1 To CountWords2(runtime_settings, ",")
runtime_setting = GetWord2(runtime_settings, i, ",") // E.g. STOP_AT
runtime_setting_key = GetWord2(runtime_setting, 1, "=")
runtime_setting_default_value = GetWord2(runtime_setting, 2, "=")
If Not isSettingDefinedInTest(runtime_setting_key) Then
runtime_setting_value = getSetting(runtime_setting_key, runtime_setting_default_value) // Use getSetting() To try To find a setting from another source, even though it's Not defined in a test source. If that fails, Then use the Default value.
setting_definitions = setting_definitions + "Const CBUNIT_"+runtime_setting_key+" = " + cbUnit_Quote + runtime_setting_value + cbUnit_Quote + cbUnit_EOL
EndIf
Next i
Return setting_definitions
EndFunction
// Returns a string of hook functions that are not defined in the current test_*.cb file.
Function EnsureHookFunctionsAreDefined()
Dim result$, i, hook_function_details$, hook_function_name$, count_parameters, is_defined, parameters$
result = ""
// Iterate Each possible hook Function
For i = 1 To CountWords2(CBUNIT_HOOK_FUNCTION_NAMES, " ")
hook_function_details = GetWord2(CBUNIT_HOOK_FUNCTION_NAMES, i, " ")
hook_function_name = GetWord2(hook_function_details, 1, ":")
count_parameters = GetWord2(hook_function_details, 2, ":")
If Not isHookFunctionDefined(hook_function_name) Then
// Define an empty Function
parameters = Countdown("_$", ",", count_parameters) // Produces a String of parameter variable names like "_0,_1,_2"
result = result + "Function "+hook_function_name+"("+parameters+") : EndFunction" + cbUnit_EOL
EndIf
Next i
Return result
EndFunction
Function GenerateListOfTestFunctionsAndConstants(test_file_path$)
Dim test_file, line_$, test_function_name$, hook_function_name$, constant_name$, setting_name$, constant_value$
ResetTestFunctions()
ResetHookFunctions()
ResetSettingsDefinedInTest()
test_file = OpenToRead(test_file_path)
While Not EOF(test_file)
line_$ = Trim(ReadLine(test_file))
If PatternCompare(CBUNIT_TEST_FUNCTION_PATTERN, line_, False) Then
// This Line contains a test_*() Function definition
// Mark up that we can call this Function
test_function_name$ = Trim(GetBetween(line_, "Function", "(")) // TODO: Fix Case sensitivity problem with the word Function.
NewTestFunction(test_function_name)
ElseIf PatternCompare(CBUNIT_HOOK_FUNCTION_PATTERN, line_, False) Then
// This Line contains a hook_*() Function, which is a special hook used by cbUnit
hook_function_name$ = Trim(GetBetween(line_, "Function", "(")) // TODO: Fix Case sensitivity problem with the word Function.
NewHookFunction(hook_function_name)
ElseIf PatternCompare(CBUNIT_CONSTANT_PATTERN, line_, False) Then
// This Line contains a CBUNIT_* constant definition
// Mark up the value of it
constant_name$ = Trim(GetBetween(line_, "Const", "=")) // TODO: Fix Case sensitivity problem with the word Const.
setting_name$ = IReplace(constant_name, "CBUNIT_", "")
constant_value$ = Trim(Mid(line_, InStr(line_, "=")+1))
If Left(constant_value,1) = Chr(34) Then // Chr(34) is "
// Remove quotes
constant_value = GetBetween(constant_value, Chr(34), Chr(34))
EndIf
setSetting(setting_name, constant_value, "test")
EndIf
Wend
CloseFile test_file
EndFunction
Function GenerateListOfTestFiles(test_paths$)
Dim count_test_paths, i, test_path$, file_name$
Dim found_file.FoundFile
count_test_paths = CountWords2(test_paths, "|")
For i = 1 To count_test_paths
test_path$ = GetWord2(test_paths, i, "|")
If FileExists(test_path) And False=IsDirectory(test_path) Then
// This path is a single test_*.cb file
NewTestFile(test_path)
ElseIf IsDirectory(test_path) Then
// This path is a directory containing test_*.cb files
// Search For all *.cb files (ListFiles() does Not support filtering by prefix, so we might get files that do Not start with "test_")
ListFiles(test_path, "cb", True) // True: recursive search
For found_file = Each FoundFile
// Now ensure that the file has a "test_" prefix
If PatternCompare(CBUNIT_TEST_FILE_PATTERN, found_file\name, False) Then
// This is a test_*.cb file
NewTestFile(found_file\absolute_path)
EndIf
Next found_file
Else
// This path is wrong
Return HandleError("getListOfTestFiles", "File or directory "+test_path+" does n"+"ot exist.")
EndIf
Next i
EndFunction
//**
// @param string initialization_directory An absolute path to the directory where "tests" folder and "cbUnit.cmd" file will be created.
//*
Function InitializeUnitTestsInDirectory(initialization_directory$)
Dim message$, tests_folder_path$, cbUnitCmd_file_path$, cbUnitCmd_file, file_name$
Dim f.FoundFile
tests_folder_path = initialization_directory + "\" + CBUNIT_TESTS_DIRECTORY
cbUnitCmd_file_path = initialization_directory + "\cbUnit.cmd"
If FileExists(cbUnitCmd_file_path) Then
message = message + "'cbUnit.cmd' already exists here: " + cbUnitCmd_file_path + cbUnit_EOL
Else
cbUnitCmd_file = OpenToWrite(cbUnitCmd_file_path)
WriteLine cbUnitCmd_file, "rem Execute cbUnit.exe. It will run test files located in: "+tests_folder_path
WriteLine cbUnitCmd_file, "start " + cbUnit_Quote+cbUnit_Quote + " "+GetEXEName()
CloseFile cbUnitCmd_file
message = message + "'cbUnit.cmd' is now created t"+"o: " + cbUnitCmd_file_path + cbUnit_EOL
EndIf
message = message + cbUnit_EOL
If FileExists(tests_folder_path) Then
message = message + "'tests' folder already exists here: " + tests_folder_path + cbUnit_EOL
Else
MakeDir tests_folder_path
If IsDirectory(tests_folder_path) Then
// Scan *.cb files in the application directory so that we can make empty test_*.cb files in the tests directory
ListFiles(initialization_directory, "cb", False)
// Create empty test_*.cb files.
For f = Each FoundFile
file_name = "test_"+f\name // It already has the .cb extension.
CloseFile OpenToWrite(tests_folder_path + "\" + file_name)
Next f
message = message + "'tests' folder is now created t"+"o: " + tests_folder_path + cbUnit_EOL
Else
message = message + "Unable t"+"o create 'tests' folder t"+"o: " + tests_folder_path + cbUnit_EOL
EndIf
EndIf
Return message
EndFunction
// Report writing
Function GenerateAndWriteReport(report_type$, report_file, messages_file_path$, test_file_path$, compiler_error$)
Dim result, test_failed, count_failures, count_successes
Dim message.cbUnit_Message
result = 0 // 0 means that nothing exceptional has happened, the test program did Not crash.
If "normal" = report_type Then
// Write report header
WriteLine report_file, ""
WriteLine report_file, test_file_path + ":"
WriteLine report_file, ""
EndIf
// Check If the compiling failed
If compiler_error Then
// Compiling failed.
// Write a syntax error message To the output file.
WriteLine report_file, compiler_error
Else
// Compiling succeeded And the test program was ran.´
ReadMessages(messages_file_path)
// Check If the test program completed without crashing
message = Last(cbUnit_Message) // The Last message should be of Type FINISH
test_failed = False
If NULL = message Then
// For some reason the test program did Not Write any message. Either it crashed in a really beginning state, Or it did Not launch at all.
HandleError("RunTests", "Test program either did n"+"ot start at all, o"+"r ended b"+"efore writing any messages.")
test_failed = True // Set this in Case HendleError() will be changed so that it will Not End the program.
ElseIf CBUNIT_MESSAGE_FINISH <> message\message_type Then
// The test program did Not End correctly. But at least it was started correctly.
// This is probably due To a Memory Access Violation Or other runtime error that happened during the test. Nothing unusual.
Select report_type
Case "normal"
WriteLine report_file, " The test program ended unexpectedly. This might be due t"+"o a Memory Access Violation (MAV) o"+"r other runtime error."
If message\message_context Then WriteLine report_file, " Crashed test: "+message\message_context+"()"
WriteLine report_file, " You'll find incomplete test results that were gotten b"+"efore the crash below."
WriteLine report_file, ""
Case "crash_plan"
WriteLine report_file, " Also the crash plan program ended unexpectedly. This might be due t"+"o the same reason why the original test program crashed."
If message\message_context Then WriteLine report_file, " Crashed f"+"unction: "+message\message_context+"()"
WriteLine report_file, ""
EndSelect
test_failed = True
result = 1 // 1 means that the test program crashed
EndIf
// Continue reporting regardless If the program crashed Or Not
Select report_type
Case "normal"
// Reset counters
count_failures = 0
count_successes = 0
// Count failures And successes
For message = Each cbUnit_Message
If CBUNIT_MESSAGE_FAILURE = message\message_type Then
// Count a failure
test_failed = True
count_failures + 1
ElseIf CBUNIT_MESSAGE_SUCCESS = message\message_type Then
// Count a success
count_successes + 1
EndIf
Next message
// Check If we have failure messages To Write.
If test_failed Then
// We have some failures.
// If they are test assertion failures, report them. (If Not, Then they are already reported above).
If count_failures > 0 Then
// We have test assertion failures.
// Iterate messages again To Read the failure messages.
WriteLine report_file, " Failures:"
For message = Each cbUnit_Message
If CBUNIT_MESSAGE_FAILURE = message\message_type Then
WriteLine report_file, " " + message\message_context+"():"
WriteLine report_file, " " + message\message_content
EndIf
Next message
WriteLine report_file, ""
EndIf
Else
// All tests passed.
WriteLine report_file, " PASSED."
EndIf
// Write a summary
WriteLine report_file, " " + (count_successes+count_failures) + " total assertions."
WriteLine report_file, " " + count_successes + " succeeded assertions."
WriteLine report_file, " " + count_failures + " failed assertions."
WriteLine report_file, ""
Case "crash_plan"
If Not test_failed Then // The wrod "test" is actually misleading here, in this Case it just means whether the program execution failed (= crashed) Or Not.
// Only Write this message If the crash plan program did Not crash.
WriteLine report_file, " Crash plan program executed successfully."
WriteLine report_file, ""
EndIf
EndSelect
EndIf
Return result
EndFunction
// Type handling
Function NewTestFile(path$)
Dim test_file.cbUnit_TestFile
test_file = New(cbUnit_TestFile)
test_file\id = ConvertToInteger(test_file)
test_file\path = path
Return test_file\id
EndFunction
Function NewTestFunction(function_name$)
Dim test_function.cbUnit_TestFunction
test_function = New(cbUnit_TestFunction)
test_function\id = ConvertToInteger(test_function)
test_function\function_name = function_name
Return test_function\id
EndFunction
Function NewHookFunction(function_name$)
Dim hook_function.cbUnit_HookFunction
hook_function = New(cbUnit_HookFunction)
hook_function\id = ConvertToInteger(hook_function)
hook_function\function_name = function_name
Return hook_function\id
EndFunction
Function NewMessage(message_type, message_context$, message_content$)
Dim message.cbUnit_Message
message = New(cbUnit_Message)
message\message_type = message_type
message\message_context = message_context
message\message_content = message_content
EndFunction
Function ResetTestFunctions()
Dim test_function.cbUnit_TestFunction
For test_function = Each cbUnit_TestFunction
Delete test_function
Next test_function
EndFunction
Function ResetHookFunctions()
Dim hook_function.cbUnit_HookFunction
For hook_function = Each cbUnit_HookFunction
Delete hook_function
Next hook_function
EndFunction
Function ResetMessages()
Dim message.cbUnit_Message
For message = Each cbUnit_Message
Delete message
Next message
EndFunction
// Settings
// source: "test" if the setting was set via a constant in a test_*.cb file. Currently there are no other source possibilities, but might be in the future.
Function setSetting(name$, value$, source$)
Dim setting.cbUnit_Setting
name = Upper(name)
setting = getSettingInstance(name)
If setting = NULL Then setting.cbUnit_Setting = New(cbUnit_Setting)
setting\id = ConvertToInteger(setting)
setting\name = name
setting\value = value
setting\source = source // Even If the setting was already loaded from another source, update here the latest source information.
EndFunction
Function getSetting(name$, if_not_found$="")
Dim setting.cbUnit_Setting
setting = getSettingInstance(name)
If setting = NULL Then Return if_not_found
Return setting\value
EndFunction
Function getSettingInstance(name$)
Dim setting.cbUnit_Setting
name = Upper(name)
For setting = Each cbUnit_Setting
If setting\name = name Then Return setting\id
Next setting
Return 0
EndFunction
// Messages (from test application)
Function ReadMessages(read_file_path$)
Dim file, message_type, message_context$, message_content$
// Preparations
ResetMessages() // Remove old messages from memory, they are already handled.
If Not FileExists(read_file_path) Then Return HandleError("ReadMessages", "Message file does n"+"ot exist.")
file = OpenToRead(read_file_path)
If EOF(file) Then Return HandleError("ReadMessages", "Message file exists but is empty.")
// Read messages
While Not EOF(file)
// Check message start
If 255 <> ReadByte(file) Then Return HandleError("ReadMessages", "Corrupted message. Wrong start value.")
// Read message Type And context
message_type = ReadByte(file) // This is 1 - 4
message_context = ReadString(file) // This is a Function name
// Read message content (If exists)
If CBUNIT_MESSAGE_FAILURE = message_type Then
message_content = ReadString(file)
EndIf
// Check message End
If 127 <> ReadByte(file) Then Return HandleError("ReadMessages", "Corrupted message. Wrong e"+"nd value.")
// All ok
NewMessage(message_type, message_context, message_content)
Wend
// Finish
CloseFile file
DeleteFile read_file_path
Return True
EndFunction
// Tells if a certain setting is defined as a constant in the current test_*.cb file. This information is needed to be able to declare those constants automatically in case if they are not defined in the test file. Otherwise FVD will break and handling default values for runtime settings would be difficult.
Function isSettingDefinedInTest(name$)
Dim setting.cbUnit_Setting
setting = getSettingInstance(name)
If NULL <> setting Then
Return setting\source = "test"
EndIf
Return False
EndFunction
Function isHookFunctionDefined(hook_function_name$)
Dim defined_hook_function.cbUnit_HookFunction
// Iterate hook functions defined in the test_*.cb file
For defined_hook_function = Each cbUnit_HookFunction
If defined_hook_function\function_name = hook_function_name Then Return True
Next defined_hook_function
Return False
EndFunction
Function ResetSettingsDefinedInTest()
Dim setting.cbUnit_Setting
For setting = Each cbUnit_Setting
If setting\source = "test" Then Delete setting // TODO: This will have a problem in the future If settings can be defined in other places in addition To test files, because there should be a way To revert back To a previous setting that a test file's setting has just temporarily overridden.
Next setting
EndFunction
// Error handling
Function HandleError(function_name$, message$)
MakeError function_name+"(): "+message
EndFunction
// Misc
Function Countdown(pattern$, separator$, count)
Dim i, result$
For i = 0 To count-1
If result Then result + separator
result = result + Replace(pattern, "$", Str(i))
Next i
Return result
EndFunction
Function TruncateFile(file_path$)
CloseFile OpenToWrite(file_path)
EndFunction
// Start main program
MainProgram()