1 | import os, os.path, sys, platform, re, copy |
---|
2 | import traceback # for custom printing of exception trace/stack |
---|
3 | import errno # for delete_file_if_present() |
---|
4 | import argparse |
---|
5 | from subprocess import Popen, PIPE |
---|
6 | from time import sleep |
---|
7 | import telnetlib |
---|
8 | |
---|
9 | # detecting CYGWIN with anaconda windows python is tricky, as all standard methods consider they are running under Windows/win32/nt. |
---|
10 | # Consequently, os.linesep is set incorrectly to '\r\n', so we resort to environment variable to fix this. Note that this would |
---|
11 | # likely give incorrect results for python installed under cygwin, so if ever needed, we should diffrentiate these two situations. |
---|
12 | #print(platform.system()) |
---|
13 | #print(sys.platform) |
---|
14 | #for a,b in os.environ.items(): #prints all environment variables... |
---|
15 | # if 'cyg' in a or 'cyg' in b: #...that contain 'cyg' and therefore may be useful for detecting that we are running under cygwin |
---|
16 | # print(a,b) |
---|
17 | CYGWIN='HOME' in os.environ and 'cygwin' in os.environ['HOME'] |
---|
18 | if CYGWIN: |
---|
19 | os.linesep='\n' #fix wrong value (suitable for Windows) |
---|
20 | |
---|
21 | |
---|
22 | import comparison # our source |
---|
23 | import globals # our source |
---|
24 | |
---|
25 | |
---|
26 | |
---|
27 | |
---|
28 | def test(args, test_name, input, output_net, output_msg, exe_prog, exeargs): |
---|
29 | print(test_name, end=" ") |
---|
30 | command = prepare_exe_with_name(exe_prog) |
---|
31 | command += exeargs |
---|
32 | if len(output_net) > 0: |
---|
33 | command += globals.EXENETMODE |
---|
34 | if args.valgrind: |
---|
35 | command = globals.EXEVALGRINDMODE + command |
---|
36 | |
---|
37 | p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE) |
---|
38 | |
---|
39 | if len(output_net) > 0: |
---|
40 | sleep(10 if args.valgrind else 1) # time for the server to warm up |
---|
41 | tn = telnetlib.Telnet("localhost", 9009) |
---|
42 | tn.write(bytes(input, "UTF-8")) |
---|
43 | sleep(2) # time for the server to respond... |
---|
44 | # if we had a command in the frams server protocol to close the connection gracefully, then we could use read_all() instead of the trick with sleep()+read_very_eager()+close() |
---|
45 | stdnet = tn.read_very_eager().decode().split("\n") # the server uses "\n" as the end-of-line character on each platform |
---|
46 | tn.close() # after this, the server is supposed to close by itself (the -N option) |
---|
47 | input = "" |
---|
48 | # under Windows, p.stderr.read() and p.stdout.read() block while the process works, under linux it may be different |
---|
49 | # http://stackoverflow.com/questions/3076542/how-can-i-read-all-availably-data-from-subprocess-popen-stdout-non-blocking?rq=1 |
---|
50 | # http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python |
---|
51 | # p.terminate() #this was required when the server did not have the -N option |
---|
52 | # stderrdata=p.stderr.read() #fortunately it is possible to reclaim (a part of?) stream contents after the process is killed... under Windows this is the ending of the stream |
---|
53 | |
---|
54 | (stdoutdata, stderrdata) = p.communicate(bytes(input, "UTF-8")) # the server process ends... |
---|
55 | stdoutdata = stdoutdata.decode() # bytes to str |
---|
56 | stderrdata = stderrdata.decode() # bytes to str |
---|
57 | # p.stdin.write(we) #this is not recommended because buffers can overflow and the process will hang up (and indeed it does under Windows) - so communicate() is recommended |
---|
58 | # stdout = p.stdout.read() |
---|
59 | # p.terminate() |
---|
60 | |
---|
61 | # print repr(input) |
---|
62 | # print repr(stdoutdata) |
---|
63 | |
---|
64 | stdout = stdoutdata.split(os.linesep) |
---|
65 | # print stdout |
---|
66 | stderr = stderrdata.split(os.linesep) |
---|
67 | ok = check(stdnet if len(output_net) > 0 else stdout, output_list if len(output_list) > 0 else output_net, output_msg) |
---|
68 | |
---|
69 | if p.returncode != 0 and p.returncode is not None: |
---|
70 | print(" ", p.returncode, "<- returned code") |
---|
71 | |
---|
72 | if len(stderrdata) > 0: |
---|
73 | print(" (stderr has %d lines)" % len(stderr)) |
---|
74 | # valgrind examples: |
---|
75 | # ==2176== ERROR SUMMARY: 597 errors from 50 contexts (suppressed: 35 from 8) |
---|
76 | # ==3488== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 35 from 8) |
---|
77 | if (not args.valgrind) or ("ERROR SUMMARY:" in stderrdata and " 0 errors" not in stderrdata) or (args.always_show_stderr): |
---|
78 | print(stderrdata) |
---|
79 | |
---|
80 | if not ok and args.stop: |
---|
81 | sys.exit("First test failure, stopping early.") |
---|
82 | return ok |
---|
83 | |
---|
84 | |
---|
85 | def compare(jest, goal, was_compared_to): |
---|
86 | compare = comparison.Comparison(jest, goal) |
---|
87 | if compare.equal: |
---|
88 | print("\r", globals.ANSI_SETGREEN + " ok" + globals.ANSI_RESET) |
---|
89 | else: |
---|
90 | print("\r", globals.ANSI_SETRED + " FAIL\7" + globals.ANSI_RESET) |
---|
91 | try: |
---|
92 | print(compare.result) |
---|
93 | except UnicodeEncodeError: |
---|
94 | print(compare.result.encode("utf-8")) #only encode as utf8 when it is actually used, but this looks incorrect and perhaps should not be needed if the console supports utf8? |
---|
95 | #sys.stdout.buffer.write(compare.result.encode("utf-8")) #prints nothing (on cygwin) |
---|
96 | failed_result_filename = compare.list2_file + was_compared_to |
---|
97 | if failed_result_filename == '': |
---|
98 | failed_result_filename = '_test' |
---|
99 | f = open(os.path.join(globals.THISDIR, failed_result_filename + '.Failed-output'), 'w', encoding='utf8') # files are easier to compare than stdout |
---|
100 | print('\n'.join(jest)+'\n', end="", file=f) # not sure why one last empty line is always lost (or one too much is expected?), adding here... |
---|
101 | f = open(os.path.join(globals.THISDIR, failed_result_filename + '.Failed-goal'), 'w', encoding='utf8') # files are easier to compare than stdout |
---|
102 | print('\n'.join(goal), end="", file=f) |
---|
103 | return compare.equal |
---|
104 | |
---|
105 | |
---|
106 | def remove_prefix(text, prefix): |
---|
107 | return text[len(prefix):] if text.startswith(prefix) else text |
---|
108 | |
---|
109 | |
---|
110 | def check(stdout, output_net, output_msg): |
---|
111 | actual_out_msg = [] |
---|
112 | if len(output_net) > 0: # in case of the server, there is no filtering |
---|
113 | for line in stdout: |
---|
114 | actual_out_msg.append(line) |
---|
115 | return compare(actual_out_msg, output_net, '') |
---|
116 | else: |
---|
117 | FROMSCRIPT = "Script.Message: " |
---|
118 | beginnings = tuple(["[" + v + "] " for v in ("INFO", "WARN", "ERROR", "CRITICAL")]) # there is also "DEBUG" |
---|
119 | header_begin = 'VMNeuronManager.autoload: Neuro classes added: ' # header section printed when the simulator is created |
---|
120 | header_end = "UserScripts.autoload: " # ending of the header section |
---|
121 | now_in_header = False |
---|
122 | for line in stdout: |
---|
123 | if now_in_header: |
---|
124 | if header_end in line: # "in" because multithreaded simulators prefix their messages with their numerical id, e.g. #12/... |
---|
125 | now_in_header = False |
---|
126 | continue |
---|
127 | else: |
---|
128 | if header_begin in line: # as above |
---|
129 | now_in_header = True |
---|
130 | continue |
---|
131 | line = remove_prefix(line, beginnings[0]) # cut out [INFO], other prefixes we want to leave as they are |
---|
132 | line = remove_prefix(line, FROMSCRIPT) # cut out FROMSCRIPT |
---|
133 | actual_out_msg.append(line) |
---|
134 | if actual_out_msg[-1] == '': # empty line at the end which is not present in our "goal" contents |
---|
135 | actual_out_msg.pop() |
---|
136 | return compare(actual_out_msg, output_msg, '') |
---|
137 | |
---|
138 | |
---|
139 | def delete_file_if_present(filename): |
---|
140 | print('"%s" (%s)' % (filename, "the file was present" if os.path.exists(filename) else "this file did not exist")) |
---|
141 | try: |
---|
142 | os.remove(filename) |
---|
143 | except OSError as e: |
---|
144 | if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory |
---|
145 | raise # re-raise exception if a different error occurred |
---|
146 | |
---|
147 | |
---|
148 | def reset_values(): |
---|
149 | global input_text, output_net, output_msg, test_name, ini, output_list, exeargs, exe_prog |
---|
150 | input_text = "" |
---|
151 | output_list = [] |
---|
152 | ini = "" |
---|
153 | output_net, output_msg = [], [] |
---|
154 | exeargs = [] |
---|
155 | test_name = "no-name test" |
---|
156 | |
---|
157 | |
---|
158 | def is_test_active(): |
---|
159 | global test_name |
---|
160 | if name_template == "": |
---|
161 | return True |
---|
162 | if re.match(name_template, test_name): |
---|
163 | return True |
---|
164 | return False |
---|
165 | |
---|
166 | |
---|
167 | def prepare_exe_with_name(name): |
---|
168 | if name in globals.EXENAMES: |
---|
169 | exename = copy.copy(globals.EXENAMES[name]) # without copy, the following modifications would change values in the EXENAMES table |
---|
170 | else: |
---|
171 | exename = [name] |
---|
172 | for rule in globals.EXERULES: |
---|
173 | exename[0] = re.sub(rule[0], rule[1], exename[0]) |
---|
174 | if CYGWIN: #somehow for anaconda under cygwin, re.sub() works incorrectly and 'anyname' with rule ('(.*)', '../\\1') yields '../anyname../' |
---|
175 | exename=['../'+name] |
---|
176 | return exename |
---|
177 | |
---|
178 | |
---|
179 | def print_exception(exc): |
---|
180 | print("\n"+("-"*60),'begin exception') |
---|
181 | print(exc) # some exceptions have an empty traceback, they only provide one-line exception name |
---|
182 | traceback.print_exc() |
---|
183 | print("-"*60,'end exception') |
---|
184 | |
---|
185 | |
---|
186 | def main(): |
---|
187 | global input_text, name_template, test_name, exe_prog, exeargs |
---|
188 | name_template = "" |
---|
189 | exeargs = [] |
---|
190 | |
---|
191 | parser = argparse.ArgumentParser() |
---|
192 | parser.add_argument("-val", "--valgrind", help="Use valgrind", action="store_true") |
---|
193 | parser.add_argument("-c", "--nocolor", help="Don't use color output", action="store_true") |
---|
194 | parser.add_argument("-f", "--file", help="File name with tests", required=True) |
---|
195 | parser.add_argument("-tp", "--tests-path", help="tests directory, files containing test definitions, inputs and outputs are relative to this directory, default is '" + globals.THISDIR + "'") |
---|
196 | parser.add_argument("-fp", "--files-path", help="files directory, files tested by OUTFILECOMPARE are referenced relative to this directory, default is '" + globals.FILESDIR + "'") |
---|
197 | parser.add_argument("-wp", "--working-path", help="working directory, test executables are launched after chdir to this directory, default is '" + globals.EXEDIR + "'") |
---|
198 | parser.add_argument("-n", "--name", help="Test name (regexp)") # e.g. '^((?!python).)*$' = these tests which don't have the word "python" in their name |
---|
199 | parser.add_argument("-s", "--stop", help="Stops on first difference", action="store_true") |
---|
200 | parser.add_argument("-ds", "--diffslashes", help="Discriminate between slashes (consider / and \\ different)", action="store_true") |
---|
201 | parser.add_argument("-err", "--always-show-stderr", help="Always print stderr (by default it is hidden if 0 errors in valgrind)", action="store_true") |
---|
202 | parser.add_argument("-e", "--exe", help="Regexp 'search=replace' rule(s) transforming executable name(s) into paths (eg. '(.*)=path/to/\\1.exe')", action='append') # in the example, double backslash is just for printing |
---|
203 | parser.add_argument("-p", "--platform", help="Override platform identifier (referencing platform specific files " + globals.SPEC_INSERTPLATFORMDEPENDENTFILE + "), default:sys.platform (win32,linux2)") |
---|
204 | args = parser.parse_args() |
---|
205 | if args.valgrind: |
---|
206 | print("Using valgrind...") |
---|
207 | if args.diffslashes: |
---|
208 | globals.DIFFSLASHES = args.diffslashes |
---|
209 | if args.file: |
---|
210 | main_test_filename = args.file |
---|
211 | if args.tests_path: |
---|
212 | globals.THISDIR = args.tests_path |
---|
213 | if args.files_path: |
---|
214 | globals.FILESDIR = args.files_path |
---|
215 | if args.working_path: |
---|
216 | globals.EXEDIR = args.working_path |
---|
217 | if args.name: |
---|
218 | name_template = args.name |
---|
219 | if args.exe: |
---|
220 | for e in args.exe: |
---|
221 | search, replace = e.split('=', 1) |
---|
222 | globals.EXERULES.append((search, replace)) |
---|
223 | if args.platform: |
---|
224 | globals.PLATFORM = args.platform |
---|
225 | |
---|
226 | os.chdir(globals.EXEDIR) |
---|
227 | |
---|
228 | globals.init_colors(args) |
---|
229 | |
---|
230 | fin = open(os.path.join(globals.THISDIR, args.file), encoding='utf8') |
---|
231 | reset_values() |
---|
232 | exe_prog = "default" # no longer in reset_values (exe: persists across tests) |
---|
233 | outfile = [] |
---|
234 | tests_failed = 0 |
---|
235 | tests_total = 0 |
---|
236 | dotOK = "" |
---|
237 | for line in fin: |
---|
238 | line = globals.stripEOL(line) |
---|
239 | if len(line) == 0 or line.startswith("#"): |
---|
240 | continue |
---|
241 | line = line.split(":", 1) |
---|
242 | # print line |
---|
243 | command = line[0] |
---|
244 | if command == "TESTNAME": |
---|
245 | reset_values() |
---|
246 | test_name = line[1] |
---|
247 | elif command == "arg": |
---|
248 | exeargs.append(line[1]) |
---|
249 | elif command == "exe": |
---|
250 | exe_prog = line[1] |
---|
251 | elif command == "in": |
---|
252 | input_text += line[1] + "\n" |
---|
253 | elif command == "out-net": |
---|
254 | output_net.append(line[1]) |
---|
255 | elif command == "out-file": |
---|
256 | outfile.append(line[1]) |
---|
257 | elif command == "out-mesg": |
---|
258 | output_msg.append(line[1]) |
---|
259 | elif command == "out": |
---|
260 | output_list.append(line[1]) |
---|
261 | elif command == "DELETEFILENOW": |
---|
262 | if is_test_active(): |
---|
263 | print("\t ", command, end=" ") |
---|
264 | delete_file_if_present(os.path.join(globals.FILESDIR, line[1])) |
---|
265 | elif command == "OUTFILECLEAR": |
---|
266 | outfile = [] |
---|
267 | elif command == "OUTFILECOMPARE": |
---|
268 | if is_test_active(): |
---|
269 | print("\t", command, '"%s"' % line[1], end=" ") |
---|
270 | try: |
---|
271 | contents = [] |
---|
272 | with open(os.path.join(globals.FILESDIR, line[1]), 'r', encoding='utf8') as main_test_filename: |
---|
273 | for line in main_test_filename: |
---|
274 | contents.append(globals.stripEOL(line)) |
---|
275 | ok = compare(contents, outfile, '_file') # +line[1] |
---|
276 | except Exception as e: # could also 'raise' for some types of exceptions if we wanted |
---|
277 | print_exception(e) |
---|
278 | ok = 0 |
---|
279 | tests_failed += int(not ok) |
---|
280 | tests_total += 1 |
---|
281 | dotOK += '.' if ok else test_name[0] if len(test_name)>0 else '?' |
---|
282 | elif command == "RUNTEST": |
---|
283 | if is_test_active(): |
---|
284 | print("\t", command, end=" ") |
---|
285 | try: |
---|
286 | ok = test(args, test_name, input_text, output_net, output_msg, exe_prog, exeargs) |
---|
287 | except Exception as e: # could also 'raise' for some types of exceptions if we wanted |
---|
288 | print_exception(e) |
---|
289 | ok = 0 |
---|
290 | tests_failed += int(not ok) |
---|
291 | tests_total += 1 |
---|
292 | dotOK += '.' if ok else test_name[0] if len(test_name)>0 else '?' |
---|
293 | else: |
---|
294 | raise Exception("Don't know what to do with this line in test file: ", line) |
---|
295 | |
---|
296 | return (tests_failed, tests_total, dotOK) |
---|
297 | |
---|
298 | |
---|
299 | if __name__ == "__main__": |
---|
300 | tests_failed, tests_total, dotOK = main() |
---|
301 | print("TestsDotOK:%s(%d)" % (dotOK, tests_failed)); |
---|
302 | print("%d / %d failed tests" % (tests_failed, tests_total)) |
---|
303 | sys.exit(tests_failed) # return the number of failed tests as exit code ("error level") to shell |
---|