Skip to content

Commit 395c368

Browse files
committed
Add new test type pcode_check for checking generated bytecode
1 parent dbc116d commit 395c368

File tree

6 files changed

+157
-50
lines changed

6 files changed

+157
-50
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
build
22
.vs
33
.vscode
4-
source/compiler/tests/*.amx

source/compiler/tests/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.amx
2+
*.asm
3+
*.lst

source/compiler/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ if(PYTHONINTERP_FOUND)
55
COMMAND ${PYTHON_EXECUTABLE}
66
${CMAKE_CURRENT_SOURCE_DIR}/run_tests.py
77
-c $<TARGET_FILE:pawncc>
8+
-d $<TARGET_FILE:pawndisasm>
89
-r $<TARGET_FILE:pawnruns>
910
-i ../../../include
1011
DEPENDS pawncc
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
'test_type': 'pcode_check',
3+
'code_pattern': r"""
4+
[0-9a-f]+ proc
5+
[0-9a-f]+ push.c 00000000
6+
[0-9a-f]+ push.c 00000004
7+
[0-9a-f]+ sysreq.c 00000000
8+
[0-9a-f]+ stack 00000008
9+
[0-9a-f]+ zero.pri
10+
[0-9a-f]+ retn
11+
"""
12+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include <console>
2+
3+
main() {
4+
printf("Hello World!");
5+
}

source/compiler/tests/run_tests.py

Lines changed: 136 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,56 @@
1010
parser = argparse.ArgumentParser()
1111
parser.add_argument('-c', '--compiler',
1212
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)')
1416
parser.add_argument('-i', '--include',
1517
dest='include_dirs',
1618
action='append',
1719
help='add custom include directories for compile tests')
1820
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)')
2122
options = parser.parse_args(sys.argv[1:])
2223

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+
2343
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
3463

3564
class OutputCheckTest:
3665
def __init__(self, name, errors=None, extra_args=None):
@@ -42,25 +71,75 @@ def run(self):
4271
args = [self.name + '.pwn']
4372
if self.extra_args is not None:
4473
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)
4875
if self.errors is None:
4976
if process.returncode != 0:
5077
result = False
5178
self.fail_reason = """
5279
No errors specified and process exited with non-zero status
5380
"""
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\nExpected 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\nExpected 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\nErrors:\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\nOutput:\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\nExpected 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
64143

65144
class RuntimeTest:
66145
def __init__(self, name, output, should_fail):
@@ -69,31 +148,29 @@ def __init__(self, name, output, should_fail):
69148
self.should_fail = should_fail
70149

71150
def run(self):
72-
process = run_compiler([self.name + '.pwn'])
73-
stdout, stderr = process.communicate()
151+
process, stdout, stderr = run_compiler([self.name + '.pwn'])
74152
if process.returncode != 0:
75153
self.fail_reason = \
76154
'Compiler exited with status {}'.format(process.returncode)
77-
errors = stderr.decode('utf-8')
155+
errors = stderr
78156
if errors:
79157
self.fail_reason += '\n\nErrors:\n\n{}'.format(errors)
80158
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)
92166
if not self.should_fail and process.returncode != 0:
93167
self.fail_reason = (
94168
'Runner exited with status {}\n\nOutput: {}'
95169
).format(process.returncode, output)
96170
return False
171+
172+
output = strip(output)
173+
expected_output = strip(self.output)
97174
if output != expected_output:
98175
self.fail_reason = (
99176
'Output didn\'t match\n\nExpected output:\n\n{}\n\n'
@@ -104,33 +181,42 @@ def run(self):
104181

105182
tests = []
106183
num_tests_disabled = 0
184+
107185
for meta_file in glob.glob('*.meta'):
108186
name = os.path.splitext(meta_file)[0]
109187
metadata = eval(open(meta_file).read(), None, None)
110188
if metadata.get('disabled'):
111189
num_tests_disabled += 1
112190
continue
191+
113192
test_type = metadata['test_type']
114193
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')))
120203
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')))
124208
else:
125209
raise KeyError('Unknown test type: ' + test_type)
126210

127211
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'))
129214
if num_tests_disabled > 0:
130215
sys.stdout.write(' ({} DISABLED)'.format(num_tests_disabled))
131216
sys.stdout.write('\n\n')
132217

133218
num_tests_failed = 0
219+
134220
for test in tests:
135221
sys.stdout.write('Running ' + test.name + '... ')
136222
if not test.run():
@@ -143,6 +229,7 @@ def run(self):
143229
sys.stdout.write('PASSED\n')
144230

145231
num_tests_passed = len(tests) - num_tests_failed
232+
146233
if num_tests_failed > 0:
147234
print('\n{} TEST{} PASSED, {} FAILED'.format(
148235
num_tests_passed,

0 commit comments

Comments
 (0)