1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-03-09 15:40:10 +00:00

[FunC] Enrich testing framework, add code hash checking

@code_hash to match (boc) hash of compiled.fif against expected.
While being much less flexible than @fif_codegen, it nevertheless
gives a guarantee of bytecode stability on compiler modifications.
This commit is contained in:
Aleksandr Kirsanov 2024-04-27 14:32:16 +05:00
parent 18050a7591
commit c74e49d467
No known key found for this signature in database
GPG key ID: B758BBAA01FFB3D3
10 changed files with 105 additions and 2 deletions

View file

@ -88,6 +88,10 @@ class CompareFifCodegenError(Exception):
pass
class CompareCodeHashError(Exception):
pass
class FuncTestCaseInputOutput:
"""
In positive tests, there are several testcases "input X should produce output Y".
@ -226,6 +230,21 @@ class FuncTestCaseFifCodegen:
return cmd_pattern == cmd_output and (comment_pattern is None or comment_pattern == comment_output)
class FuncTestCaseExpectedHash:
"""
@code_hash checks that hash of compiled output.fif matches the provided value.
It's used to "record" code boc hash and to check that it remains the same on compiler modifications.
Being much less flexible than @fif_codegen, it nevertheless gives a guarantee of bytecode stability.
"""
def __init__(self, expected_hash: str):
self.code_hash = expected_hash
def check(self, fif_code_hash: str):
if self.code_hash != fif_code_hash:
raise CompareCodeHashError("expected %s, actual %s" % (self.code_hash, fif_code_hash))
class FuncTestFile:
def __init__(self, func_filename: str, artifacts_folder: str):
self.line_idx = 0
@ -235,6 +254,7 @@ class FuncTestFile:
self.stderr_includes: list[FuncTestCaseStderr] = []
self.input_output: list[FuncTestCaseInputOutput] = []
self.fif_codegen: list[FuncTestCaseFifCodegen] = []
self.expected_hash: FuncTestCaseExpectedHash | None = None
def parse_input_from_func_file(self):
with open(self.func_filename, "r") as fd:
@ -256,6 +276,8 @@ class FuncTestFile:
self.fif_codegen.append(FuncTestCaseFifCodegen(self.parse_string_value(lines), True))
elif line.startswith("@fif_codegen"):
self.fif_codegen.append(FuncTestCaseFifCodegen(self.parse_string_value(lines), False))
elif line.startswith("@code_hash"):
self.expected_hash = FuncTestCaseExpectedHash(self.parse_string_value(lines, False)[0])
self.line_idx = self.line_idx + 1
if len(self.input_output) == 0 and not self.compilation_should_fail:
@ -263,7 +285,7 @@ class FuncTestFile:
if len(self.input_output) != 0 and self.compilation_should_fail:
raise ParseInputError("TESTCASE present, but compilation_should_fail")
def parse_string_value(self, lines: list[str]) -> list[str]:
def parse_string_value(self, lines: list[str], allow_multiline = True) -> list[str]:
# a tag must be followed by a space (single-line), e.g. '@stderr some text'
# or be a multi-line value, surrounded by """
line = lines[self.line_idx]
@ -274,6 +296,8 @@ class FuncTestFile:
raise ParseInputError('%s value is empty (not followed by a string or a multiline """)' % line)
if is_single_line and is_multi_line:
raise ParseInputError('%s value is both single-line and followed by """' % line[:pos_sp])
if is_multi_line and not allow_multiline:
raise ParseInputError("%s value should be single-line" % line)
if is_single_line:
return [line[pos_sp + 1:].strip()]
@ -312,6 +336,8 @@ class FuncTestFile:
f.write("\"%s\" include <s constant code\n" % self.get_compiled_fif_filename())
for t in self.input_output:
f.write("%s %d code 1 runvmx abort\"exitcode is not 0\" .s cr { drop } depth 1- times\n" % (t.input, t.method_id))
if self.expected_hash is not None:
f.write("\"%s\" include hash .s\n" % self.get_compiled_fif_filename())
res = subprocess.run([FIFT_EXECUTABLE, self.get_runner_fif_filename()], capture_output=True, timeout=10)
exit_code = res.returncode
@ -319,6 +345,10 @@ class FuncTestFile:
stdout = str(res.stdout, "utf-8")
stdout_lines = [x.strip() for x in stdout.split("\n")]
stdout_lines = [x for x in stdout_lines if x != ""]
fif_code_hash = None
if self.expected_hash is not None: # then the last stdout line is a hash
fif_code_hash = stdout_lines[-1]
stdout_lines = stdout_lines[:-1]
if exit_code != 0:
raise FiftExecutionFailedError("fift exit_code = %d" % exit_code, stderr)
@ -335,6 +365,9 @@ class FuncTestFile:
for fif_codegen in self.fif_codegen:
fif_codegen.check(fif_output)
if self.expected_hash is not None:
self.expected_hash.check(fif_code_hash)
def run_all_tests(tests: list[str]):
for ti in range(len(tests)):
@ -382,6 +415,10 @@ def run_all_tests(tests: list[str]):
print(" Was compiled to:", testcase.get_compiled_fif_filename(), file=sys.stderr)
print(open(testcase.get_compiled_fif_filename()).read(), file=sys.stderr)
exit(2)
except CompareCodeHashError as e:
print(" Mismatch in code hash:", e, file=sys.stderr)
print(" Was compiled to:", testcase.get_compiled_fif_filename(), file=sys.stderr)
exit(2)
tests = CmdLineOptions(sys.argv).find_tests()