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:
parent
18050a7591
commit
c74e49d467
10 changed files with 105 additions and 2 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue