10
10
parser = argparse .ArgumentParser ()
11
11
parser .add_argument ('-c' , '--compiler' ,
12
12
required = True ,
13
- help = 'path to compiler executable (pawncc or pawncc.exe)' )
13
+ help = 'path to compiler executable (pawncc)' )
14
+ parser .add_argument ('-d' , '--disassembler' ,
15
+ help = 'path to disassembler executable (pawndisasm)' )
14
16
parser .add_argument ('-i' , '--include' ,
15
17
dest = 'include_dirs' ,
16
18
action = 'append' ,
17
19
help = 'add custom include directories for compile tests' )
18
20
parser .add_argument ('-r' , '--runner' ,
19
- required = True ,
20
- help = 'path to runner executable (pawnrun or pawnrun.exe)' )
21
+ help = 'path to runner executable (pawnruns)' )
21
22
options = parser .parse_args (sys .argv [1 :])
22
23
24
+ def run_command (args , executable = None , merge_stderr = False ):
25
+ process = subprocess .Popen (args ,
26
+ executable = executable ,
27
+ stdout = subprocess .PIPE ,
28
+ stderr = subprocess .PIPE ,
29
+ universal_newlines = True )
30
+ stdout , stderr = process .communicate ()
31
+ stdout = stdout .decode ('utf-8' )
32
+ stderr = stderr .decode ('utf-8' )
33
+ if merge_stderr :
34
+ output = ''
35
+ if stdout :
36
+ output += stdout
37
+ if stderr :
38
+ output += stderr
39
+ return (process , output )
40
+ else :
41
+ return (process , stdout , stderr )
42
+
23
43
def run_compiler (args ):
24
- process_args = [';+' , '-(+' ]
25
- if options .include_dirs is not None :
26
- for dir in options .include_dirs :
27
- process_args .append ('-i' + dir )
28
- if args is not None :
29
- process_args += args
30
- return subprocess .Popen (executable = options .compiler ,
31
- args = process_args ,
32
- stdout = subprocess .PIPE ,
33
- stderr = subprocess .PIPE )
44
+ final_args = [';+' , '-(+' ]
45
+ if options .include_dirs is not None :
46
+ for dir in options .include_dirs :
47
+ final_args .append ('-i' + dir )
48
+ if args is not None :
49
+ final_args += args
50
+ return run_command (executable = options .compiler , args = final_args )
51
+
52
+ def normalize_newlines (s ):
53
+ return s .replace ('\r ' , '' )
54
+
55
+ def remove_asm_comments (s ):
56
+ return re .sub (r'\s*;.*\n' , '\n ' , s )
57
+
58
+ def strip (s ):
59
+ return s .strip (' \t \r \n ' )
60
+
61
+ def parse_asm_listing (dump ):
62
+ return None
34
63
35
64
class OutputCheckTest :
36
65
def __init__ (self , name , errors = None , extra_args = None ):
@@ -42,25 +71,75 @@ def run(self):
42
71
args = [self .name + '.pwn' ]
43
72
if self .extra_args is not None :
44
73
args += extra_args
45
- process = run_compiler (args = args )
46
- stdout , stderr = process .communicate ()
47
- result = True
74
+ process , stdout , stderr = run_compiler (args = args )
48
75
if self .errors is None :
49
76
if process .returncode != 0 :
50
77
result = False
51
78
self .fail_reason = """
52
79
No errors specified and process exited with non-zero status
53
80
"""
54
- else :
55
- errors = stderr .decode ('utf-8' ).strip (' \t \r \n ' ).replace ('\r ' , '' )
56
- expected_errors = self .errors .strip (' \t \r \n ' ).replace ('\r ' , '' )
57
- if errors != expected_errors :
58
- result = False
59
- self .fail_reason = (
60
- 'Error output didn\' t match\n \n Expected errors:\n \n {}\n \n '
61
- 'Actual errors:\n \n {}'
62
- ).format (expected_errors , errors )
63
- return result
81
+ return False
82
+
83
+ errors = strip (stderr )
84
+ expected_errors = strip (self .errors )
85
+ if errors != expected_errors :
86
+ self .fail_reason = (
87
+ 'Error output didn\' t match\n \n Expected errors:\n \n {}\n \n '
88
+ 'Actual errors:\n \n {}'
89
+ ).format (expected_errors , errors )
90
+ return False
91
+ return True
92
+
93
+ class PCodeCheckTest :
94
+ def __init__ (self ,
95
+ name ,
96
+ code_pattern = None ,
97
+ extra_args = None ):
98
+ self .name = name
99
+ self .code_pattern = code_pattern
100
+ self .extra_args = extra_args
101
+
102
+ def run (self ):
103
+ args = ['-d0' , self .name + '.pwn' ]
104
+ if self .extra_args is not None :
105
+ args += extra_args
106
+ process , stdout , stderr = run_compiler (args = args )
107
+ if process .returncode != 0 :
108
+ self .fail_reason = \
109
+ 'Compiler exited with status {}' .format (process .returncode )
110
+ errors = stderr
111
+ if errors :
112
+ self .fail_reason += '\n \n Errors:\n \n {}' .format (errors )
113
+ return False
114
+
115
+ if options .disassembler is None :
116
+ self .fail_reason = 'Disassembler path is not set, can\' t run this test'
117
+ return False
118
+ process , output = run_command ([
119
+ options .disassembler ,
120
+ self .name + '.amx'
121
+ ], merge_stderr = True )
122
+ if process .returncode != 0 :
123
+ self .fail_reason = \
124
+ 'Disassembler exited with status {}' .format (process .returncode )
125
+ if output :
126
+ self .fail_reason += '\n \n Output:\n \n {}' .format (output )
127
+ return False
128
+ with open (self .name + '.lst' , 'r' ) as dump_file :
129
+ dump = dump_file .read ()
130
+ if self .code_pattern :
131
+ dump = remove_asm_comments (dump )
132
+ code_pattern = strip (normalize_newlines (self .code_pattern ))
133
+ if re .search (code_pattern , dump , re .MULTILINE ) is None :
134
+ self .fail_reason = (
135
+ 'Code didn\' t match\n \n Expected code:\n \n {}\n \n '
136
+ 'Actual code:\n \n {}'
137
+ ).format (code_pattern , dump )
138
+ return False
139
+ else :
140
+ self .fail_reason = 'Code pattern is required'
141
+ return False
142
+ return True
64
143
65
144
class RuntimeTest :
66
145
def __init__ (self , name , output , should_fail ):
@@ -69,31 +148,29 @@ def __init__(self, name, output, should_fail):
69
148
self .should_fail = should_fail
70
149
71
150
def run (self ):
72
- process = run_compiler ([self .name + '.pwn' ])
73
- stdout , stderr = process .communicate ()
151
+ process , stdout , stderr = run_compiler ([self .name + '.pwn' ])
74
152
if process .returncode != 0 :
75
153
self .fail_reason = \
76
154
'Compiler exited with status {}' .format (process .returncode )
77
- errors = stderr . decode ( 'utf-8' )
155
+ errors = stderr
78
156
if errors :
79
157
self .fail_reason += '\n \n Errors:\n \n {}' .format (errors )
80
158
return False
81
- process = subprocess .Popen ([options .runner , self .name + '.amx' ],
82
- stdout = subprocess .PIPE ,
83
- stderr = subprocess .PIPE )
84
- stdout , stderr = process .communicate ()
85
- output = ''
86
- if stdout is not None :
87
- output = stdout .decode ('utf-8' )
88
- if stderr is not None :
89
- output += stderr .decode ('utf-8' )
90
- output = output .strip (' \t \r \n ' ).replace ('\r ' , '' )
91
- expected_output = self .output .strip (' \t \r \n ' ).replace ('\r ' , '' )
159
+
160
+ if options .runner is None :
161
+ self .fail_reason = 'Runner path is not set, can\' t run this test'
162
+ return False
163
+ process , output = run_command ([
164
+ options .runner , self .name + '.amx'
165
+ ], merge_stderr = True )
92
166
if not self .should_fail and process .returncode != 0 :
93
167
self .fail_reason = (
94
168
'Runner exited with status {}\n \n Output: {}'
95
169
).format (process .returncode , output )
96
170
return False
171
+
172
+ output = strip (output )
173
+ expected_output = strip (self .output )
97
174
if output != expected_output :
98
175
self .fail_reason = (
99
176
'Output didn\' t match\n \n Expected output:\n \n {}\n \n '
@@ -104,33 +181,42 @@ def run(self):
104
181
105
182
tests = []
106
183
num_tests_disabled = 0
184
+
107
185
for meta_file in glob .glob ('*.meta' ):
108
186
name = os .path .splitext (meta_file )[0 ]
109
187
metadata = eval (open (meta_file ).read (), None , None )
110
188
if metadata .get ('disabled' ):
111
189
num_tests_disabled += 1
112
190
continue
191
+
113
192
test_type = metadata ['test_type' ]
114
193
if test_type == 'output_check' :
115
- tests .append (OutputCheckTest (name = name ,
116
- errors = metadata .get ('errors' ),
117
- extra_args = metadata .get ('extra_args' )))
118
- elif test_type == 'crash' :
119
- tests .append (CrashTest (name = name ))
194
+ tests .append (OutputCheckTest (
195
+ name = name ,
196
+ errors = metadata .get ('errors' ),
197
+ extra_args = metadata .get ('extra_args' )))
198
+ elif test_type == 'pcode_check' :
199
+ tests .append (PCodeCheckTest (
200
+ name = name ,
201
+ code_pattern = metadata .get ('code_pattern' ),
202
+ extra_args = metadata .get ('extra_args' )))
120
203
elif test_type == 'runtime' :
121
- tests .append (RuntimeTest (name = name ,
122
- output = metadata .get ('output' ),
123
- should_fail = metadata .get ('should_fail' )))
204
+ tests .append (RuntimeTest (
205
+ name = name ,
206
+ output = metadata .get ('output' ),
207
+ should_fail = metadata .get ('should_fail' )))
124
208
else :
125
209
raise KeyError ('Unknown test type: ' + test_type )
126
210
127
211
num_tests = len (tests )
128
- sys .stdout .write ('DISCOVERED {} TEST{}' .format (num_tests , '' if num_tests == 1 else 'S' ))
212
+ sys .stdout .write (
213
+ 'DISCOVERED {} TEST{}' .format (num_tests , '' if num_tests == 1 else 'S' ))
129
214
if num_tests_disabled > 0 :
130
215
sys .stdout .write (' ({} DISABLED)' .format (num_tests_disabled ))
131
216
sys .stdout .write ('\n \n ' )
132
217
133
218
num_tests_failed = 0
219
+
134
220
for test in tests :
135
221
sys .stdout .write ('Running ' + test .name + '... ' )
136
222
if not test .run ():
@@ -143,6 +229,7 @@ def run(self):
143
229
sys .stdout .write ('PASSED\n ' )
144
230
145
231
num_tests_passed = len (tests ) - num_tests_failed
232
+
146
233
if num_tests_failed > 0 :
147
234
print ('\n {} TEST{} PASSED, {} FAILED' .format (
148
235
num_tests_passed ,
0 commit comments