diff --git a/crypto/func/auto-tests/run_tests.js b/crypto/func/auto-tests/run_tests.js index 28a0d536..bef2ed42 100644 --- a/crypto/func/auto-tests/run_tests.js +++ b/crypto/func/auto-tests/run_tests.js @@ -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 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 } diff --git a/crypto/func/auto-tests/run_tests.py b/crypto/func/auto-tests/run_tests.py index 3e2cb8a6..e9e393e2 100644 --- a/crypto/func/auto-tests/run_tests.py +++ b/crypto/func/auto-tests/run_tests.py @@ -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 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 -} diff --git a/crypto/func/auto-tests/tests/asm_arg_order.fc b/crypto/func/auto-tests/tests/asm_arg_order.fc index b53419e3..84a8a926 100644 --- a/crypto/func/auto-tests/tests/asm_arg_order.fc +++ b/crypto/func/auto-tests/tests/asm_arg_order.fc @@ -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 -} diff --git a/crypto/func/auto-tests/tests/co1.fc b/crypto/func/auto-tests/tests/co1.fc index 44067243..515c6bc6 100644 --- a/crypto/func/auto-tests/tests/co1.fc +++ b/crypto/func/auto-tests/tests/co1.fc @@ -57,4 +57,6 @@ _ main() { {- TESTCASE | 0 | | 0 + +@code_hash 98157584761932576648981760908342408003231747902562138202913714302941716912743 -} diff --git a/crypto/func/auto-tests/tests/s1.fc b/crypto/func/auto-tests/tests/s1.fc index 1541943d..97b19f16 100644 --- a/crypto/func/auto-tests/tests/s1.fc +++ b/crypto/func/auto-tests/tests/s1.fc @@ -51,4 +51,6 @@ _ main() { {- TESTCASE | 0 | | 0 + +@code_hash 35542701048549611407499491204004197688673151742407097578098250360682143458214 -} diff --git a/crypto/func/auto-tests/tests/try-func.fc b/crypto/func/auto-tests/tests/try-func.fc index 5678965e..fb2a968a 100644 --- a/crypto/func/auto-tests/tests/try-func.fc +++ b/crypto/func/auto-tests/tests/try-func.fc @@ -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 -} diff --git a/crypto/func/auto-tests/tests/unbalanced_ret_nested.fc b/crypto/func/auto-tests/tests/unbalanced_ret_nested.fc index 7ab4bdf0..a54a0dd1 100644 --- a/crypto/func/auto-tests/tests/unbalanced_ret_nested.fc +++ b/crypto/func/auto-tests/tests/unbalanced_ret_nested.fc @@ -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 -} diff --git a/crypto/func/auto-tests/tests/w6.fc b/crypto/func/auto-tests/tests/w6.fc index 9b91cdf7..44337c12 100644 --- a/crypto/func/auto-tests/tests/w6.fc +++ b/crypto/func/auto-tests/tests/w6.fc @@ -14,4 +14,6 @@ int main(int x) { {- method_id | in | out TESTCASE | 0 | 0 | 1 + +@code_hash 36599880583276393028571473830850694081778552118303309411432666239740650614479 -}