Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test for via-ir codegen import bug #14565

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions scripts/common/cmdline_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import subprocess
from pathlib import Path
from shutil import rmtree
from tempfile import mkdtemp
from textwrap import dedent
from typing import List
from typing import Optional

from bytecodecompare.prepare_report import FileReport
from bytecodecompare.prepare_report import parse_cli_output


DEFAULT_PREAMBLE = dedent("""
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.0;
""")


def inside_temporary_dir(prefix):
"""
Creates a temporary directory, enters the directory and executes the function inside it.
Restores the previous working directory after executing the function.
"""
def tmp_dir_decorator(fn):
previous_dir = os.getcwd()
def f(*args, **kwargs):
try:
tmp_dir = mkdtemp(prefix=prefix)
os.chdir(tmp_dir)
result = fn(*args, **kwargs)
rmtree(tmp_dir)
r0qs marked this conversation as resolved.
Show resolved Hide resolved
return result
finally:
os.chdir(previous_dir)
return f
return tmp_dir_decorator


def solc_bin_report(solc_binary: str, input_files: List[Path], via_ir: bool) -> FileReport:
"""
Runs the solidity compiler binary
"""

output = subprocess.check_output(
[solc_binary, '--bin'] +
input_files +
(['--via-ir'] if via_ir else []),
encoding='utf8',
)
return parse_cli_output('', output)


def save_bytecode(bytecode_path: Path, reports: FileReport, contract: Optional[str] = None):
with open(bytecode_path, 'w', encoding='utf8') as f:
for report in reports.contract_reports:
if contract is None or report.contract_name == contract:
bytecode = report.bytecode if report.bytecode is not None else '<NO BYTECODE>'
f.write(f'{report.contract_name}: {bytecode}\n')


def add_preamble(source_path: Path, preamble: str = DEFAULT_PREAMBLE):
for source in source_path.glob('**/*.sol'):
with open(source, 'r+', encoding='utf8') as f:
content = f.read()
f.seek(0, 0)
f.write(preamble + content)
20 changes: 20 additions & 0 deletions scripts/common/git_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import subprocess
from pathlib import Path
from shutil import which


def run_git_command(command):
Expand All @@ -17,3 +19,21 @@ def git_current_branch():

def git_commit_hash(ref: str = 'HEAD'):
return run_git_command(['git', 'rev-parse', '--verify', ref])


def git_diff(file_a: Path, file_b: Path) -> int:
if which('git') is None:
raise RuntimeError('git not found.')

return subprocess.run([
'git',
'diff',
'--color',
'--word-diff=plain',
'--word-diff-regex=.',
'--ignore-space-change',
'--ignore-blank-lines',
'--exit-code',
file_a,
file_b,
], encoding='utf8', check=False).returncode
31 changes: 13 additions & 18 deletions scripts/splitSources.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
import os
import traceback

hasMultipleSources = False
createdSources = []


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

# recursive call if there is another source
else:
writeSourceToFile(lines[1+idx:])
break
return [srcName] + writeSourceToFile(lines[1+idx:])

return [srcName]

if __name__ == '__main__':
filePath = sys.argv[1]
def split_sources(filePath, suppress_output = False):
sys.excepthook = uncaught_exception_hook

try:
# decide if file has multiple sources
with open(filePath, mode='r', encoding='utf8', newline='') as f:
lines = f.read().splitlines()
if len(lines) >= 1 and lines[0][:12] == "==== Source:":
hasMultipleSources = True
writeSourceToFile(lines)

if hasMultipleSources:
srcString = ""
for src in createdSources:
for src in writeSourceToFile(lines):
srcString += src + ' '
print(srcString)
sys.exit(0)
else:
sys.exit(1)
if not suppress_output:
print(srcString)
return 0
return 1

except UnicodeDecodeError as ude:
print("UnicodeDecodeError in '" + filePath + "': " + str(ude))
print("This is expected for some tests containing invalid utf8 sequences. "
"Exception will be ignored.")
sys.exit(2)
return 2


if __name__ == '__main__':
sys.exit(split_sources(sys.argv[1]))
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
==== Source: A.sol ====
import "@/D.sol";
import "B.sol";

contract A is B {
function a() public pure {
e();
}
}

==== Source: B.sol ====
import "C.sol";

abstract contract B is C {}

==== Source: C.sol ====
abstract contract C {
function c() public pure returns (uint) {
return 0;
}
}

==== Source: @/D.sol ====
import "@/E.sol";

==== Source: @/E.sol ====
function e() pure returns (bytes memory returndata) {
return "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3

import os
import sys
from pathlib import Path
from textwrap import dedent

# pylint: disable=wrong-import-position
PROJECT_ROOT = Path(__file__).parents[3]
sys.path.insert(0, str(PROJECT_ROOT / 'scripts'))

from common.cmdline_helpers import add_preamble
from common.cmdline_helpers import inside_temporary_dir
from common.cmdline_helpers import save_bytecode
from common.cmdline_helpers import solc_bin_report
from common.git_helpers import git_diff
from splitSources import split_sources


@inside_temporary_dir(Path(__file__).parent.name)
def test_bytecode_equivalence():
source_file_path = Path(__file__).parent / 'inputs.sol'
split_sources(source_file_path, suppress_output=True)
add_preamble(Path.cwd())

solc_binary = os.environ.get('SOLC')
if solc_binary is None:
raise RuntimeError(dedent("""\
`solc` compiler not found.
Please ensure you set the SOLC environment variable
with the correct path to the compiler's binary.
"""))

# Whether a file is passed to the compiler explicitly or only discovered when traversing imports
# may affect the order in which files are processed and result in different AST IDs.
# This, however, must not result in different bytecode being generated.
save_bytecode(Path('A.bin'), solc_bin_report(solc_binary, [Path('A.sol')], via_ir=True))
save_bytecode(Path('AB.bin'), solc_bin_report(solc_binary, [Path('A.sol'), Path('B.sol')], via_ir=True))
return git_diff(Path('A.bin'), Path('AB.bin'))


if __name__ == '__main__':
sys.exit(test_bytecode_equivalence())