1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-02-12 19:22:37 +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

@ -92,6 +92,9 @@ class CompareOutputError extends Error {
class CompareFifCodegenError extends Error { class CompareFifCodegenError extends Error {
} }
class CompareCodeHashError extends Error {
}
/* /*
* In positive tests, there are several testcases "input X should produce output Y". * In positive tests, there are several testcases "input X should produce output Y".
@ -236,6 +239,22 @@ class FuncTestCaseFifCodegen {
} }
} }
/*
* @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.
*/
class FuncTestCaseExpectedHash {
constructor(/**string*/ expected_hash) {
this.code_hash = expected_hash
}
check(fif_code_hash) {
if (this.code_hash !== fif_code_hash)
throw new CompareCodeHashError(`expected ${this.code_hash}, actual ${fif_code_hash}`)
}
}
class FuncTestFile { class FuncTestFile {
constructor(/**string*/ func_filename, /**string*/ artifacts_folder) { constructor(/**string*/ func_filename, /**string*/ artifacts_folder) {
@ -249,6 +268,8 @@ class FuncTestFile {
this.input_output = [] this.input_output = []
/** @type {FuncTestCaseFifCodegen[]} */ /** @type {FuncTestCaseFifCodegen[]} */
this.fif_codegen = [] this.fif_codegen = []
/** @type {FuncTestCaseExpectedHash | null} */
this.expected_hash = null
} }
parse_input_from_func_file() { parse_input_from_func_file() {
@ -270,6 +291,8 @@ class FuncTestFile {
this.fif_codegen.push(new FuncTestCaseFifCodegen(this.parse_string_value(lines), true)) this.fif_codegen.push(new FuncTestCaseFifCodegen(this.parse_string_value(lines), true))
} else if (line.startsWith("@fif_codegen")) { } else if (line.startsWith("@fif_codegen")) {
this.fif_codegen.push(new FuncTestCaseFifCodegen(this.parse_string_value(lines), false)) this.fif_codegen.push(new FuncTestCaseFifCodegen(this.parse_string_value(lines), false))
} else if (line.startsWith("@code_hash")) {
this.expected_hash = new FuncTestCaseExpectedHash(this.parse_string_value(lines, false)[0])
} }
this.line_idx++ this.line_idx++
} }
@ -281,7 +304,7 @@ class FuncTestFile {
} }
/** @return {string[]} */ /** @return {string[]} */
parse_string_value(/**string[]*/ lines) { parse_string_value(/**string[]*/ lines, allow_multiline = true) {
// a tag must be followed by a space (single-line), e.g. '@stderr some text' // a tag must be followed by a space (single-line), e.g. '@stderr some text'
// or be a multi-line value, surrounded by """ // or be a multi-line value, surrounded by """
const line = lines[this.line_idx] const line = lines[this.line_idx]
@ -292,6 +315,8 @@ class FuncTestFile {
throw new ParseInputError(`${line} value is empty (not followed by a string or a multiline """)`) throw new ParseInputError(`${line} value is empty (not followed by a string or a multiline """)`)
if (is_single_line && is_multi_line) if (is_single_line && is_multi_line)
throw new ParseInputError(`${line.substring(0, pos_sp)} value is both single-line and followed by """`) throw new ParseInputError(`${line.substring(0, pos_sp)} value is both single-line and followed by """`)
if (is_multi_line && !allow_multiline)
throw new ParseInputError(`${line} value should be single-line`);
if (is_single_line) if (is_single_line)
return [line.substring(pos_sp + 1).trim()] return [line.substring(pos_sp + 1).trim()]
@ -337,6 +362,8 @@ class FuncTestFile {
let runner = `"${this.get_compiled_fif_filename()}" include <s constant code\n` let runner = `"${this.get_compiled_fif_filename()}" include <s constant code\n`
for (let t of this.input_output) for (let t of this.input_output)
runner += `${t.input} ${t.method_id} code 1 runvmx abort"exitcode is not 0" .s cr { drop } depth 1- times\n` runner += `${t.input} ${t.method_id} code 1 runvmx abort"exitcode is not 0" .s cr { drop } depth 1- times\n`
if (this.expected_hash !== null)
runner += `"${this.get_compiled_fif_filename()}" include hash .s\n`
fs.writeFileSync(this.get_runner_fif_filename(), runner) fs.writeFileSync(this.get_runner_fif_filename(), runner)
} }
@ -345,6 +372,11 @@ class FuncTestFile {
stderr = (res.stderr || res.error).toString() stderr = (res.stderr || res.error).toString()
stdout = (res.stdout || '').toString() stdout = (res.stdout || '').toString()
let stdout_lines = stdout.split("\n").map(x => x.trim()).filter(s => s.length > 0) let stdout_lines = stdout.split("\n").map(x => x.trim()).filter(s => s.length > 0)
let fif_code_hash = null
if (this.expected_hash !== null) { // then the last stdout line is a hash
fif_code_hash = stdout_lines[stdout_lines.length - 1]
stdout_lines = stdout_lines.slice(0, stdout_lines.length - 1)
}
if (exit_code) if (exit_code)
throw new FiftExecutionFailedError(`fift exit_code = ${exit_code}`, stderr) throw new FiftExecutionFailedError(`fift exit_code = ${exit_code}`, stderr)
@ -360,6 +392,9 @@ class FuncTestFile {
for (let fif_codegen of this.fif_codegen) for (let fif_codegen of this.fif_codegen)
fif_codegen.check(fif_output) fif_codegen.check(fif_output)
} }
if (this.expected_hash !== null)
this.expected_hash.check(fif_code_hash)
} }
} }
@ -408,6 +443,10 @@ async function run_all_tests(/**string[]*/ tests) {
print(" Was compiled to:", testcase.get_compiled_fif_filename()) print(" Was compiled to:", testcase.get_compiled_fif_filename())
print(fs.readFileSync(testcase.get_compiled_fif_filename(), 'utf-8')) print(fs.readFileSync(testcase.get_compiled_fif_filename(), 'utf-8'))
process.exit(2) process.exit(2)
} else if (e instanceof CompareCodeHashError) {
print(" Mismatch in code hash:", e.message)
print(" Was compiled to:", testcase.get_compiled_fif_filename())
process.exit(2)
} }
throw e throw e
} }

View file

@ -88,6 +88,10 @@ class CompareFifCodegenError(Exception):
pass pass
class CompareCodeHashError(Exception):
pass
class FuncTestCaseInputOutput: class FuncTestCaseInputOutput:
""" """
In positive tests, there are several testcases "input X should produce output Y". 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) 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: class FuncTestFile:
def __init__(self, func_filename: str, artifacts_folder: str): def __init__(self, func_filename: str, artifacts_folder: str):
self.line_idx = 0 self.line_idx = 0
@ -235,6 +254,7 @@ class FuncTestFile:
self.stderr_includes: list[FuncTestCaseStderr] = [] self.stderr_includes: list[FuncTestCaseStderr] = []
self.input_output: list[FuncTestCaseInputOutput] = [] self.input_output: list[FuncTestCaseInputOutput] = []
self.fif_codegen: list[FuncTestCaseFifCodegen] = [] self.fif_codegen: list[FuncTestCaseFifCodegen] = []
self.expected_hash: FuncTestCaseExpectedHash | None = None
def parse_input_from_func_file(self): def parse_input_from_func_file(self):
with open(self.func_filename, "r") as fd: 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)) self.fif_codegen.append(FuncTestCaseFifCodegen(self.parse_string_value(lines), True))
elif line.startswith("@fif_codegen"): elif line.startswith("@fif_codegen"):
self.fif_codegen.append(FuncTestCaseFifCodegen(self.parse_string_value(lines), False)) 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 self.line_idx = self.line_idx + 1
if len(self.input_output) == 0 and not self.compilation_should_fail: 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: if len(self.input_output) != 0 and self.compilation_should_fail:
raise ParseInputError("TESTCASE present, but 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' # a tag must be followed by a space (single-line), e.g. '@stderr some text'
# or be a multi-line value, surrounded by """ # or be a multi-line value, surrounded by """
line = lines[self.line_idx] 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) raise ParseInputError('%s value is empty (not followed by a string or a multiline """)' % line)
if is_single_line and is_multi_line: if is_single_line and is_multi_line:
raise ParseInputError('%s value is both single-line and followed by """' % line[:pos_sp]) 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: if is_single_line:
return [line[pos_sp + 1:].strip()] 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()) f.write("\"%s\" include <s constant code\n" % self.get_compiled_fif_filename())
for t in self.input_output: 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)) 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) res = subprocess.run([FIFT_EXECUTABLE, self.get_runner_fif_filename()], capture_output=True, timeout=10)
exit_code = res.returncode exit_code = res.returncode
@ -319,6 +345,10 @@ class FuncTestFile:
stdout = str(res.stdout, "utf-8") stdout = str(res.stdout, "utf-8")
stdout_lines = [x.strip() for x in stdout.split("\n")] stdout_lines = [x.strip() for x in stdout.split("\n")]
stdout_lines = [x for x in stdout_lines if x != ""] 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: if exit_code != 0:
raise FiftExecutionFailedError("fift exit_code = %d" % exit_code, stderr) raise FiftExecutionFailedError("fift exit_code = %d" % exit_code, stderr)
@ -335,6 +365,9 @@ class FuncTestFile:
for fif_codegen in self.fif_codegen: for fif_codegen in self.fif_codegen:
fif_codegen.check(fif_output) 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]): def run_all_tests(tests: list[str]):
for ti in range(len(tests)): 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(" Was compiled to:", testcase.get_compiled_fif_filename(), file=sys.stderr)
print(open(testcase.get_compiled_fif_filename()).read(), file=sys.stderr) print(open(testcase.get_compiled_fif_filename()).read(), file=sys.stderr)
exit(2) 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() tests = CmdLineOptions(sys.argv).find_tests()

View file

@ -86,4 +86,6 @@ int main() {
{- {-
method_id | in | out method_id | in | out
TESTCASE | 0 | | 31415926535897932384626433832795028841971693993751058209749445923078164 TESTCASE | 0 | | 31415926535897932384626433832795028841971693993751058209749445923078164
@code_hash 84337043972311674339187056298873613816389434478842780265748859098303774481976
-} -}

View file

@ -61,6 +61,15 @@ global int xx;
return (xx, xx~inc(xx / 20), xx, xx = xx * 2, xx, xx += 1, xx); return (xx, xx~inc(xx / 20), xx, xx = xx * 2, xx, xx += 1, xx);
} }
(int, int, int, int, int) test_if_else(int x) method_id(20) {
if (x > 10) {
return (x~inc(8), x + 1, x = 1, x <<= 3, x);
} else {
xx = 9;
return (x, x~inc(-4), x~inc(-1), x >= 1, x = x + xx);
}
}
() main() { () main() {
} }
@ -75,4 +84,8 @@ TESTCASE | 16 | 100 | 100 50 105 210 210 211 211
TESTCASE | 17 | 100 | 100 50 105 210 210 211 211 TESTCASE | 17 | 100 | 100 50 105 210 210 211 211
TESTCASE | 18 | 100 | 210 210 211 211 100 50 105 TESTCASE | 18 | 100 | 210 210 211 211 100 50 105
TESTCASE | 19 | 100 | 100 50 105 210 210 211 211 TESTCASE | 19 | 100 | 100 50 105 210 210 211 211
TESTCASE | 20 | 80 | 80 89 1 8 8
TESTCASE | 20 | 9 | 9 -40 -10 -1 13
@code_hash 67078680159561921827850021610104736412668316252257881932102553152922274405210
-} -}

View file

@ -110,4 +110,6 @@ TESTCASE | 23 | | [ 11 22 33 ] [ 220 330 110 ]
TESTCASE | 24 | | [ 11 22 33 44 55 ] [ 220 330 440 110 550 ] TESTCASE | 24 | | [ 11 22 33 44 55 ] [ 220 330 440 110 550 ]
TESTCASE | 25 | | [ 22 33 ] [ 220 330 ] TESTCASE | 25 | | [ 22 33 ] [ 220 330 ]
TESTCASE | 26 | | [ 11 22 33 ] [ 220 330 110 ] TESTCASE | 26 | | [ 11 22 33 ] [ 220 330 110 ]
@code_hash 53895312198338603934600244087571743055624960603383611438828666636202841531600
-} -}

View file

@ -57,4 +57,6 @@ _ main() {
{- {-
TESTCASE | 0 | | 0 TESTCASE | 0 | | 0
@code_hash 98157584761932576648981760908342408003231747902562138202913714302941716912743
-} -}

View file

@ -51,4 +51,6 @@ _ main() {
{- {-
TESTCASE | 0 | | 0 TESTCASE | 0 | | 0
@code_hash 35542701048549611407499491204004197688673151742407097578098250360682143458214
-} -}

View file

@ -106,4 +106,6 @@ TESTCASE | 4 | 4 8 9 | 4350009
TESTCASE | 4 | 4 7 9 | 4001009 TESTCASE | 4 | 4 7 9 | 4001009
TESTCASE | 5 | 4 8 9 | 4350009 TESTCASE | 5 | 4 8 9 | 4350009
TESTCASE | 5 | 4 7 9 | 4001009 TESTCASE | 5 | 4 7 9 | 4001009
@code_hash 30648920105446086409127767431349650306796188362733360847851465122919640638913
-} -}

View file

@ -32,4 +32,6 @@ TESTCASE | 0 | -5 -4 | 111 -70
TESTCASE | 0 | -4 3 | -7 40 TESTCASE | 0 | -4 3 | -7 40
TESTCASE | 0 | -4 -5 | -7 1110 TESTCASE | 0 | -4 -5 | -7 1110
TESTCASE | 0 | -4 -4 | -7 -70 TESTCASE | 0 | -4 -4 | -7 -70
@code_hash 68625253347714662162648433047986779710161195298061582217368558479961252943991
-} -}

View file

@ -14,4 +14,6 @@ int main(int x) {
{- {-
method_id | in | out method_id | in | out
TESTCASE | 0 | 0 | 1 TESTCASE | 0 | 0 | 1
@code_hash 36599880583276393028571473830850694081778552118303309411432666239740650614479
-} -}