1+ """
2+ TestSuite and Test objects and some helper functions for running simple tests
3+ on command line programs.
4+
5+ Tries to do a lot and not too much while providing simple clean output for
6+ test results. Uses ansi color codes to color the output. This will make test
7+ output hard to read on terminals that do not support them.
8+
9+ Lots of different kind of tests that feed a command line program some input and
10+ expect some output can be run with these tools. However, it is likely that some
11+ things will need to be adjusted. The test suite has been written with a lot of
12+ small methods that are called by other methods. This gives it a bit of a
13+ template pattern to replace any little peice as needed. This may be to read
14+ input or output differently or to perform a different check to determine
15+ success or failure or to add new data and operations while keeping the same
16+ general pattern. The helper functions are also separate and not TestSuite
17+ methods to allow their use in different ways without always subclassing
18+ TestSuite. To see some project specific uses see the lieutenant-64 repository.
19+
20+ TODO Make this it's own project and repo. Then add some examples of how it
21+ could be used to test certain kinds of program and maybe some simple extensions
22+ to cover common use cases not covered by the base class.
23+ """
124import subprocess as sp
225
326### Exit codes ###
1134
1235### Functions ################################################################
1336def display_totals (failed , total , reprint_failed = True , show_fail_info = True ):
37+ """Display the results of a test run to stdout.
38+
39+ failed is a list of failed Test objects. If there are any failed tests,
40+ and reprint_failed is true, they will have their individual results
41+ printed with all their info. If show_fail_info is false then the short
42+ output for each failed test result will be reprinted.
43+
44+ total is the number of tests that were run.
45+ """
1446 print ()
1547 print ()
1648 if len (failed ):
@@ -25,6 +57,14 @@ def display_totals(failed, total, reprint_failed=True, show_fail_info=True):
2557 print (f"{ GREEN } ****{ NORMAL } { total } Tests Passed { GREEN } ****{ NORMAL } " )
2658
2759def display_test_results (test , passed , num , show_fail_info = True ):
60+ """Displays a line with the test name and whether it passed or failed.
61+
62+ If show_fail_info is true more detailed output will be given with any error
63+ text and exit code followed by the expected output and the actual output.
64+
65+ num is the number of the test in it's test suite and will be printed before
66+ the test name.
67+ """
2868 print (f"{ f'{ num } :' :<3} { test .name :<70} " , end = "" )
2969 if passed :
3070 print (f"[{ GREEN } PASS{ NORMAL } ]" )
@@ -41,6 +81,14 @@ def display_test_results(test, passed, num, show_fail_info=True):
4181 print ()
4282
4383def compile_program (command_list ):
84+ """Given a list of the compilation command and it's arguments will run
85+ a subprocess to compile a program.
86+
87+ Captures error output and the exit code and prints them to stderr if the
88+ compilation fails.
89+
90+ Retruns True on successful compilation and False otherwise.
91+ """
4492 command = " " .join (command_list )
4593 print (f"Compiling: { command } " )
4694 comp = sp .run (command_list , capture_output = True )
@@ -53,6 +101,11 @@ def compile_program(command_list):
53101 return True
54102
55103def run_test_suites (test_suites ):
104+ """Runs each test suite in a list of tests suites.
105+
106+ Sets all test output for the running of the tests to False so that only
107+ the test names and results are printed during the run of each suite.
108+ At the end reprints each failed test with all of it's verbose info."""
56109 failed = []
57110 total = 0
58111 for suite in test_suites :
@@ -69,6 +122,42 @@ def run_test_suites(test_suites):
69122
70123# TODO implement behaviour for show_line_diff=True
71124class TestSuite :
125+ """A collection of tests for a command line program that will be run
126+ as a group.
127+
128+ Takes a test suite name, a command for the command line program to run for
129+ each test, and a list of tests.
130+
131+ The input to the program defaults to stdin, but program_source can be
132+ passed a file name to read the input to pass to the program under test's
133+ subprocess. In a similar way program output can specify where to expect
134+ the actual output. If it is stdout the programs output will be read from
135+ stdout. If it is a filename that file will be read instead.
136+
137+ input_is_file tells the test suite that the program under test expects to
138+ read from a file instead of being given input. If this is true then
139+ program source must be set as the filename and any program input will be
140+ written to that file so the program under test can use it to get test
141+ input.
142+
143+ expected_is_file tells the test suite that the expected output needs to be
144+ read from a file. This might be handy if the expected output is very large
145+ and it is impractical to specify it in a string when creating the Test
146+ object.
147+
148+ print_end_info will tell the suite that when it is finished running it
149+ should display some information about the run of the suite. If this is
150+ true and reprint_failed is true then each failed test will be reprinted
151+ after the tests have all finished running.
152+
153+ show_fail_info specifies that when a test fails a verbose output of
154+ failure information should be printed. This might contain error information
155+ and will always contain the expected output vs the actual output.
156+
157+ show_line_diff(unimplemented) is indended to give a view of the difference
158+ between the expected and actual instead of printing them completely.
159+ However it has not been setup yet.
160+ """
72161 def __init__ (self , name , command ,
73162 tests = [], compile_command = None ,
74163 program_source = "stdin" , program_output = "stdout" ,
@@ -90,6 +179,9 @@ def __init__(self, name, command,
90179 self .failed = []
91180
92181 def run (self ):
182+ """Runs each test in order and displays the results for each run
183+ and the totals if print_end_info is true.
184+ """
93185 print ()
94186 print (f"======== { self .name } ========" )
95187 if self .compile_command :
@@ -102,6 +194,8 @@ def run(self):
102194 self .reprint_failed , self .show_fail_info )
103195
104196 def execute (self ):
197+ """Executes each test and prints the individual test info
198+ according to the printing flags that are set."""
105199 for i , t in enumerate (self .tests ):
106200 if self .program_source != "stdin" :
107201 self .write_program_input (t )
@@ -118,24 +212,41 @@ def execute(self):
118212 self .failed .append ((i + 1 , t ))
119213
120214 def write_program_input (self , test ):
121- print ("base write" )
215+ """Writes a test's program input to a file so that it can be read
216+ by the program under test. Can be overridden if a specific Test type
217+ might need a different output type. I.e. writing a binary file."""
122218 with open (self .program_source , 'w+' ) as f :
123219 f .write (test .input )
124220
125221 def get_test_input (self , test ):
222+ """Returns the test input. If the test expects input from a file it
223+ will be read in and returned. Otherwise the test input will be
224+ returned as a byte string. The reason for this is that Subprocess.run
225+ expects program input as a byte string. It can also be overriden if
226+ getting or creating the test input from a test is more complex. I.e.
227+ test input is given as a hex string and needs to be converted to the
228+ appropriate representation before being returned as a byte string."""
126229 if self .input_is_file :
127230 with open (test .input , 'rb' ) as f :
128231 return f .read ()
129232 else :
130233 return test .input ().encode ('utf-8' )
131234
132235 def check (self , test , proc ):
236+ """Checks to see if the process result output matches the expected
237+ output. To pass a test must have matching expected and actual
238+ output as well as return 0 for the exit code."""
133239 actual = self .get_actual (test , proc )
134240 expected = self .get_expected (test , proc )
135241 test .exit_code = proc .returncode
136242 return actual == expected and test .exit_code == EXIT_SUCCESS
137243
138244 def get_actual (self , test , proc ):
245+ """Returns the string of actual output after stripping whitespace
246+ from the ends. If the output is expected to be in a file it reads it
247+ from, otherwise the output captured to stdout by the process run is
248+ returned as a utf-8 string. Can be overriden to provide different
249+ behaviour such as returning an ascii string."""
139250 if self .program_output != "stdout" :
140251 with open (self .program_output , 'r' ):
141252 test .actual = f .read ().strip ()
@@ -144,6 +255,9 @@ def get_actual(self, test, proc):
144255 return test .actual
145256
146257 def get_expected (self , test , proc ):
258+ """Retrns the string of expected output. As get_actual it will read
259+ from a file or from the test's expected propery. Strips all leading
260+ and trailing whitespace. Can be also be overriden."""
147261 if self .expected_is_file :
148262 with open (test .expected , 'r' ):
149263 test .expected = f .read ().strip ()
@@ -152,6 +266,20 @@ def get_expected(self, test, proc):
152266 return test .expected
153267
154268class Test :
269+ """A simple data object for a test used to test the running of
270+ a cli program.
271+
272+ name is the name that will be printed when showing test results.
273+
274+ input_ and expected are strings for data to pass to the program
275+ under test and what to compare its output to.
276+
277+ show_fail_info tells display functions whether or not to show detailed
278+ information on test failure.
279+
280+ members actual, stderr, and exit_code are provided to add information to
281+ a test after running it to save for processes that may need it but run
282+ in a different place than where the test was executed."""
155283 def __init__ (self , name , input_ , expected , show_fail_info = True ):
156284 self .name = name
157285 self .input = input_
0 commit comments