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:
parent
18050a7591
commit
c74e49d467
10 changed files with 105 additions and 2 deletions
|
@ -92,6 +92,9 @@ class CompareOutputError extends Error {
|
|||
class CompareFifCodegenError extends Error {
|
||||
}
|
||||
|
||||
class CompareCodeHashError extends Error {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
constructor(/**string*/ func_filename, /**string*/ artifacts_folder) {
|
||||
|
@ -249,6 +268,8 @@ class FuncTestFile {
|
|||
this.input_output = []
|
||||
/** @type {FuncTestCaseFifCodegen[]} */
|
||||
this.fif_codegen = []
|
||||
/** @type {FuncTestCaseExpectedHash | null} */
|
||||
this.expected_hash = null
|
||||
}
|
||||
|
||||
parse_input_from_func_file() {
|
||||
|
@ -270,6 +291,8 @@ class FuncTestFile {
|
|||
this.fif_codegen.push(new FuncTestCaseFifCodegen(this.parse_string_value(lines), true))
|
||||
} else if (line.startsWith("@fif_codegen")) {
|
||||
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++
|
||||
}
|
||||
|
@ -281,7 +304,7 @@ class FuncTestFile {
|
|||
}
|
||||
|
||||
/** @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'
|
||||
// or be a multi-line value, surrounded by """
|
||||
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 """)`)
|
||||
if (is_single_line && is_multi_line)
|
||||
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)
|
||||
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`
|
||||
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`
|
||||
if (this.expected_hash !== null)
|
||||
runner += `"${this.get_compiled_fif_filename()}" include hash .s\n`
|
||||
fs.writeFileSync(this.get_runner_fif_filename(), runner)
|
||||
}
|
||||
|
||||
|
@ -345,6 +372,11 @@ class FuncTestFile {
|
|||
stderr = (res.stderr || res.error).toString()
|
||||
stdout = (res.stdout || '').toString()
|
||||
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)
|
||||
throw new FiftExecutionFailedError(`fift exit_code = ${exit_code}`, stderr)
|
||||
|
@ -360,6 +392,9 @@ class FuncTestFile {
|
|||
for (let fif_codegen of this.fif_codegen)
|
||||
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(fs.readFileSync(testcase.get_compiled_fif_filename(), 'utf-8'))
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -86,4 +86,6 @@ int main() {
|
|||
{-
|
||||
method_id | in | out
|
||||
TESTCASE | 0 | | 31415926535897932384626433832795028841971693993751058209749445923078164
|
||||
|
||||
@code_hash 84337043972311674339187056298873613816389434478842780265748859098303774481976
|
||||
-}
|
||||
|
|
|
@ -61,6 +61,15 @@ global int 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() {
|
||||
}
|
||||
|
||||
|
@ -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 | 18 | 100 | 210 210 211 211 100 50 105
|
||||
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
|
||||
-}
|
||||
|
|
|
@ -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 | 25 | | [ 22 33 ] [ 220 330 ]
|
||||
TESTCASE | 26 | | [ 11 22 33 ] [ 220 330 110 ]
|
||||
|
||||
@code_hash 53895312198338603934600244087571743055624960603383611438828666636202841531600
|
||||
-}
|
||||
|
|
|
@ -57,4 +57,6 @@ _ main() {
|
|||
|
||||
{-
|
||||
TESTCASE | 0 | | 0
|
||||
|
||||
@code_hash 98157584761932576648981760908342408003231747902562138202913714302941716912743
|
||||
-}
|
||||
|
|
|
@ -51,4 +51,6 @@ _ main() {
|
|||
|
||||
{-
|
||||
TESTCASE | 0 | | 0
|
||||
|
||||
@code_hash 35542701048549611407499491204004197688673151742407097578098250360682143458214
|
||||
-}
|
||||
|
|
|
@ -106,4 +106,6 @@ TESTCASE | 4 | 4 8 9 | 4350009
|
|||
TESTCASE | 4 | 4 7 9 | 4001009
|
||||
TESTCASE | 5 | 4 8 9 | 4350009
|
||||
TESTCASE | 5 | 4 7 9 | 4001009
|
||||
|
||||
@code_hash 30648920105446086409127767431349650306796188362733360847851465122919640638913
|
||||
-}
|
||||
|
|
|
@ -32,4 +32,6 @@ TESTCASE | 0 | -5 -4 | 111 -70
|
|||
TESTCASE | 0 | -4 3 | -7 40
|
||||
TESTCASE | 0 | -4 -5 | -7 1110
|
||||
TESTCASE | 0 | -4 -4 | -7 -70
|
||||
|
||||
@code_hash 68625253347714662162648433047986779710161195298061582217368558479961252943991
|
||||
-}
|
||||
|
|
|
@ -14,4 +14,6 @@ int main(int x) {
|
|||
{-
|
||||
method_id | in | out
|
||||
TESTCASE | 0 | 0 | 1
|
||||
|
||||
@code_hash 36599880583276393028571473830850694081778552118303309411432666239740650614479
|
||||
-}
|
||||
|
|
Loading…
Reference in a new issue