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 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -86,4 +86,6 @@ int main() {
|
||||||
{-
|
{-
|
||||||
method_id | in | out
|
method_id | in | out
|
||||||
TESTCASE | 0 | | 31415926535897932384626433832795028841971693993751058209749445923078164
|
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);
|
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
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -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
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -57,4 +57,6 @@ _ main() {
|
||||||
|
|
||||||
{-
|
{-
|
||||||
TESTCASE | 0 | | 0
|
TESTCASE | 0 | | 0
|
||||||
|
|
||||||
|
@code_hash 98157584761932576648981760908342408003231747902562138202913714302941716912743
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -51,4 +51,6 @@ _ main() {
|
||||||
|
|
||||||
{-
|
{-
|
||||||
TESTCASE | 0 | | 0
|
TESTCASE | 0 | | 0
|
||||||
|
|
||||||
|
@code_hash 35542701048549611407499491204004197688673151742407097578098250360682143458214
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -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
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -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
|
||||||
-}
|
-}
|
||||||
|
|
|
@ -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
|
||||||
-}
|
-}
|
||||||
|
|
Loading…
Reference in a new issue