Skip to content

Commit ab2d395

Browse files
r0qscameel
andcommitted
Add bytecode test for multiple sources compiler at same time
Co-authored-by: Kamil Śliwak <[email protected]>
1 parent 2b7ec23 commit ab2d395

File tree

5 files changed

+172
-18
lines changed

5 files changed

+172
-18
lines changed

scripts/common/cmdline_helpers.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
import subprocess
3+
from pathlib import Path
4+
from shutil import rmtree
5+
from tempfile import mkdtemp
6+
from textwrap import dedent
7+
from typing import List
8+
from typing import Optional
9+
10+
from bytecodecompare.prepare_report import FileReport
11+
from bytecodecompare.prepare_report import parse_cli_output
12+
13+
14+
DEFAULT_PREAMBLE = dedent("""
15+
// SPDX-License-Identifier: UNLICENSED
16+
pragma solidity >=0.0;
17+
""")
18+
19+
20+
def inside_temporary_dir(prefix):
21+
"""
22+
Creates a temporary directory, enters the directory and executes the function inside it.
23+
Restores the previous working directory after executing the function.
24+
"""
25+
def tmp_dir_decorator(fn):
26+
previous_dir = os.getcwd()
27+
def f(*args, **kwargs):
28+
try:
29+
tmp_dir = mkdtemp(prefix=prefix)
30+
os.chdir(tmp_dir)
31+
result = fn(*args, **kwargs)
32+
rmtree(tmp_dir)
33+
return result
34+
finally:
35+
os.chdir(previous_dir)
36+
return f
37+
return tmp_dir_decorator
38+
39+
40+
def solc_bin_report(solc_binary: str, input_files: List[Path], via_ir: bool) -> FileReport:
41+
"""
42+
Runs the solidity compiler binary
43+
"""
44+
45+
output = subprocess.check_output(
46+
[solc_binary, '--bin'] +
47+
input_files +
48+
(['--via-ir'] if via_ir else []),
49+
encoding='utf8',
50+
)
51+
return parse_cli_output('', output)
52+
53+
54+
def save_bytecode(bytecode_path: Path, reports: FileReport, contract: Optional[str] = None):
55+
with open(bytecode_path, 'w', encoding='utf8') as f:
56+
for report in reports.contract_reports:
57+
if contract is None or report.contract_name == contract:
58+
bytecode = report.bytecode if report.bytecode is not None else '<NO BYTECODE>'
59+
f.write(f'{report.contract_name}: {bytecode}\n')
60+
61+
62+
def add_preamble(source_path: Path, preamble: str = DEFAULT_PREAMBLE):
63+
for source in source_path.glob('**/*.sol'):
64+
with open(source, 'r+', encoding='utf8') as f:
65+
content = f.read()
66+
f.seek(0, 0)
67+
f.write(preamble + content)

scripts/common/git_helpers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import subprocess
2+
from pathlib import Path
3+
from shutil import which
24

35

46
def run_git_command(command):
@@ -17,3 +19,21 @@ def git_current_branch():
1719

1820
def git_commit_hash(ref: str = 'HEAD'):
1921
return run_git_command(['git', 'rev-parse', '--verify', ref])
22+
23+
24+
def git_diff(file_a: Path, file_b: Path) -> int:
25+
if which('git') is None:
26+
raise RuntimeError('git not found.')
27+
28+
return subprocess.run([
29+
'git',
30+
'diff',
31+
'--color',
32+
'--word-diff=plain',
33+
'--word-diff-regex=.',
34+
'--ignore-space-change',
35+
'--ignore-blank-lines',
36+
'--exit-code',
37+
file_a,
38+
file_b,
39+
], encoding='utf8', check=False).returncode

scripts/splitSources.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
import os
1414
import traceback
1515

16-
hasMultipleSources = False
17-
createdSources = []
18-
1916

2017
def uncaught_exception_hook(exc_type, exc_value, exc_traceback):
2118
# The script `scripts/ASTImportTest.sh` will interpret return code 3
@@ -43,41 +40,39 @@ def writeSourceToFile(lines):
4340
if filePath:
4441
os.system("mkdir -p " + filePath)
4542
with open(srcName, mode='a+', encoding='utf8', newline='') as f:
46-
createdSources.append(srcName)
4743
for idx, line in enumerate(lines[1:]):
4844
# write to file
4945
if line[:12] != "==== Source:":
5046
f.write(line + '\n')
5147

5248
# recursive call if there is another source
5349
else:
54-
writeSourceToFile(lines[1+idx:])
55-
break
50+
return [srcName] + writeSourceToFile(lines[1+idx:])
5651

52+
return [srcName]
5753

58-
if __name__ == '__main__':
59-
filePath = sys.argv[1]
54+
def split_sources(filePath, suppress_output = False):
6055
sys.excepthook = uncaught_exception_hook
6156

6257
try:
6358
# decide if file has multiple sources
6459
with open(filePath, mode='r', encoding='utf8', newline='') as f:
6560
lines = f.read().splitlines()
6661
if len(lines) >= 1 and lines[0][:12] == "==== Source:":
67-
hasMultipleSources = True
68-
writeSourceToFile(lines)
69-
70-
if hasMultipleSources:
7162
srcString = ""
72-
for src in createdSources:
63+
for src in writeSourceToFile(lines):
7364
srcString += src + ' '
74-
print(srcString)
75-
sys.exit(0)
76-
else:
77-
sys.exit(1)
65+
if not suppress_output:
66+
print(srcString)
67+
return 0
68+
return 1
7869

7970
except UnicodeDecodeError as ude:
8071
print("UnicodeDecodeError in '" + filePath + "': " + str(ude))
8172
print("This is expected for some tests containing invalid utf8 sequences. "
8273
"Exception will be ignored.")
83-
sys.exit(2)
74+
return 2
75+
76+
77+
if __name__ == '__main__':
78+
sys.exit(split_sources(sys.argv[1]))
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
==== Source: A.sol ====
2+
import "@/D.sol";
3+
import "B.sol";
4+
5+
contract A is B {
6+
function a() public pure {
7+
e();
8+
}
9+
}
10+
11+
==== Source: B.sol ====
12+
import "C.sol";
13+
14+
abstract contract B is C {}
15+
16+
==== Source: C.sol ====
17+
abstract contract C {
18+
function c() public pure returns (uint) {
19+
return 0;
20+
}
21+
}
22+
23+
==== Source: @/D.sol ====
24+
import "@/E.sol";
25+
26+
==== Source: @/E.sol ====
27+
function e() pure returns (bytes memory returndata) {
28+
return "";
29+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
from pathlib import Path
6+
from textwrap import dedent
7+
8+
# pylint: disable=wrong-import-position
9+
PROJECT_ROOT = Path(__file__).parents[3]
10+
sys.path.insert(0, str(PROJECT_ROOT / 'scripts'))
11+
12+
from common.cmdline_helpers import add_preamble
13+
from common.cmdline_helpers import inside_temporary_dir
14+
from common.cmdline_helpers import save_bytecode
15+
from common.cmdline_helpers import solc_bin_report
16+
from common.git_helpers import git_diff
17+
from splitSources import split_sources
18+
19+
20+
@inside_temporary_dir(Path(__file__).parent.name)
21+
def test_bytecode_equivalence():
22+
source_file_path = Path(__file__).parent / 'inputs.sol'
23+
split_sources(source_file_path, suppress_output=True)
24+
add_preamble(Path.cwd())
25+
26+
solc_binary = os.environ.get('SOLC')
27+
if solc_binary is None:
28+
raise RuntimeError(dedent("""\
29+
`solc` compiler not found.
30+
Please ensure you set the SOLC environment variable
31+
with the correct path to the compiler's binary.
32+
"""))
33+
34+
# Whether a file is passed to the compiler explicitly or only discovered when traversing imports
35+
# may affect the order in which files are processed and result in different AST IDs.
36+
# This, however, must not result in different bytecode being generated.
37+
save_bytecode(Path('A.bin'), solc_bin_report(solc_binary, [Path('A.sol')], via_ir=True))
38+
save_bytecode(Path('AB.bin'), solc_bin_report(solc_binary, [Path('A.sol'), Path('B.sol')], via_ir=True))
39+
return git_diff(Path('A.bin'), Path('AB.bin'))
40+
41+
42+
if __name__ == '__main__':
43+
sys.exit(test_bytecode_equivalence())

0 commit comments

Comments
 (0)