diff --git a/scripts/bytecodecompare/prepare_report.js b/scripts/bytecodecompare/prepare_report.js new file mode 100755 index 000000000..1d1b04060 --- /dev/null +++ b/scripts/bytecodecompare/prepare_report.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node +const process = require('process') +const fs = require('fs') + +const compiler = require('./solc-js/wrapper.js')(require('./solc-js/soljson.js')) + +for (const optimize of [false, true]) +{ + for (const filename of process.argv.slice(2)) + { + if (filename !== undefined) + { + const input = { + language: 'Solidity', + sources: { + [filename]: {content: fs.readFileSync(filename).toString()} + }, + settings: { + optimizer: {enabled: optimize}, + outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}}, + "modelChecker": {"engine": "none"} + } + } + + const result = JSON.parse(compiler.compile(JSON.stringify(input))) + + if ( + !('contracts' in result) || + Object.keys(result['contracts']).length === 0 || + Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0) + ) + // NOTE: do not exit here because this may be run on source which cannot be compiled + console.log(filename + ': ') + else + for (const contractFile in result['contracts']) + for (const contractName in result['contracts'][contractFile]) + { + const contractResults = result['contracts'][contractFile][contractName] + + let bytecode = '' + let metadata = '' + + if ('evm' in contractResults && 'bytecode' in contractResults['evm'] && 'object' in contractResults['evm']['bytecode']) + bytecode = contractResults.evm.bytecode.object + + if ('metadata' in contractResults) + metadata = contractResults.metadata + + console.log(filename + ':' + contractName + ' ' + bytecode) + console.log(filename + ':' + contractName + ' ' + metadata) + } + } + } +} diff --git a/scripts/bytecodecompare/prepare_report.py b/scripts/bytecodecompare/prepare_report.py index 2697b940b..d654569a4 100755 --- a/scripts/bytecodecompare/prepare_report.py +++ b/scripts/bytecodecompare/prepare_report.py @@ -1,43 +1,149 @@ #!/usr/bin/env python3 import sys -import glob import subprocess import json +from argparse import ArgumentParser +from dataclasses import dataclass +from glob import glob +from pathlib import Path +from typing import List, Optional, Tuple, Union -SOLC_BIN = sys.argv[1] -REPORT_FILE = open("report.txt", mode="w", encoding='utf8', newline='\n') -for optimize in [False, True]: - for f in sorted(glob.glob("*.sol")): - sources = {} - sources[f] = {'content': open(f, mode='r', encoding='utf8').read()} - input_json = { - 'language': 'Solidity', - 'sources': sources, - 'settings': { - 'optimizer': { - 'enabled': optimize - }, - 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, - 'modelChecker': { "engine": 'none' } - } +@dataclass(frozen=True) +class ContractReport: + contract_name: str + file_name: Path + bytecode: Optional[str] + metadata: Optional[str] + + +@dataclass +class FileReport: + file_name: Path + contract_reports: Optional[List[ContractReport]] + + def format_report(self) -> str: + report = "" + + if self.contract_reports is None: + return f"{self.file_name}: \n" + + for contract_report in self.contract_reports: + bytecode = contract_report.bytecode if contract_report.bytecode is not None else '' + metadata = contract_report.metadata if contract_report.metadata is not None else '' + + # NOTE: Ignoring contract_report.file_name because it should always be either the same + # as self.file_name (for Standard JSON) or just the '' placeholder (for CLI). + report += f"{self.file_name}:{contract_report.contract_name} {bytecode}\n" + report += f"{self.file_name}:{contract_report.contract_name} {metadata}\n" + + return report + + +def load_source(path: Union[Path, str]) -> str: + with open(path, mode='r', encoding='utf8') as source_file: + file_content = source_file.read() + + return file_content + + +def parse_standard_json_output(source_file_name: Path, standard_json_output: str) -> FileReport: + decoded_json_output = json.loads(standard_json_output.strip()) + + if ( + 'contracts' not in decoded_json_output or + len(decoded_json_output['contracts']) == 0 or + all(len(file_results) == 0 for file_name, file_results in decoded_json_output['contracts'].items()) + ): + return FileReport(file_name=source_file_name, contract_reports=None) + + file_report = FileReport(file_name=source_file_name, contract_reports=[]) + for file_name, file_results in sorted(decoded_json_output['contracts'].items()): + for contract_name, contract_results in sorted(file_results.items()): + assert file_report.contract_reports is not None + file_report.contract_reports.append(ContractReport( + contract_name=contract_name, + file_name=Path(file_name), + bytecode=contract_results.get('evm', {}).get('bytecode', {}).get('object'), + metadata=contract_results.get('metadata'), + )) + + return file_report + + +def prepare_compiler_input(compiler_path: Path, source_file_name: Path, optimize: bool) -> Tuple[List[str], str]: + json_input: dict = { + 'language': 'Solidity', + 'sources': { + str(source_file_name): {'content': load_source(source_file_name)} + }, + 'settings': { + 'optimizer': {'enabled': optimize}, + 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, + 'modelChecker': {'engine': 'none'}, } - args = [SOLC_BIN, '--standard-json'] - if optimize: - args += ['--optimize'] - proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (out, err) = proc.communicate(json.dumps(input_json).encode('utf-8')) - try: - result = json.loads(out.decode('utf-8').strip()) - for filename in sorted(result['contracts'].keys()): - for contractName in sorted(result['contracts'][filename].keys()): - contractData = result['contracts'][filename][contractName] - if 'evm' in contractData and 'bytecode' in contractData['evm']: - REPORT_FILE.write(filename + ':' + contractName + ' ' + - contractData['evm']['bytecode']['object'] + '\n') - else: - REPORT_FILE.write(filename + ':' + contractName + ' NO BYTECODE\n') - REPORT_FILE.write(filename + ':' + contractName + ' ' + contractData['metadata'] + '\n') - except KeyError: - REPORT_FILE.write(f + ": ERROR\n") + } + + command_line = [str(compiler_path), '--standard-json'] + compiler_input = json.dumps(json_input) + + return (command_line, compiler_input) + + +def run_compiler(compiler_path: Path, source_file_name: Path, optimize: bool) -> FileReport: + (command_line, compiler_input) = prepare_compiler_input(compiler_path, Path(Path(source_file_name).name), optimize) + + process = subprocess.run( + command_line, + input=compiler_input, + encoding='utf8', + capture_output=True, + check=False, + ) + + return parse_standard_json_output(Path(source_file_name), process.stdout) + + +def generate_report(source_file_names: List[str], compiler_path: Path): + with open('report.txt', mode='w', encoding='utf8', newline='\n') as report_file: + for optimize in [False, True]: + for source_file_name in sorted(source_file_names): + try: + report = run_compiler(Path(compiler_path), Path(source_file_name), optimize) + report_file.write(report.format_report()) + except subprocess.CalledProcessError as exception: + print( + f"\n\nInterrupted by an exception while processing file " + f"'{source_file_name}' with optimize={optimize}\n\n" + f"COMPILER STDOUT:\n{exception.stdout}\n" + f"COMPILER STDERR:\n{exception.stderr}\n", + file=sys.stderr + ) + raise + except: + print( + f"\n\nInterrupted by an exception while processing file " + f"'{source_file_name}' with optimize={optimize}\n", + file=sys.stderr + ) + raise + + +def commandline_parser() -> ArgumentParser: + script_description = ( + "Generates a report listing bytecode and metadata obtained by compiling all the " + "*.sol files found in the current working directory using the provided binary." + ) + + parser = ArgumentParser(description=script_description) + parser.add_argument(dest='compiler_path', help="Solidity compiler executable") + return parser; + + +if __name__ == "__main__": + options = commandline_parser().parse_args() + generate_report( + glob("*.sol"), + Path(options.compiler_path), + ) diff --git a/scripts/bytecodecompare/storebytecode.sh b/scripts/bytecodecompare/storebytecode.sh index bddc6aa56..31094911c 100755 --- a/scripts/bytecodecompare/storebytecode.sh +++ b/scripts/bytecodecompare/storebytecode.sh @@ -46,60 +46,9 @@ TMPDIR=$(mktemp -d) git clone --depth 1 https://github.com/ethereum/solc-js.git solc-js ( cd solc-js; npm install ) cp "$REPO_ROOT/emscripten_build/libsolc/soljson.js" solc-js/ - cat > solc < report.txt + ./prepare_report.js *.sol > report.txt echo "Finished running the compiler." else "$REPO_ROOT/scripts/bytecodecompare/prepare_report.py" "$BUILD_DIR/solc/solc" diff --git a/test/scripts/fixtures/library_inherited2_sol_json_output.json b/test/scripts/fixtures/library_inherited2_sol_json_output.json new file mode 100644 index 000000000..2e48c685b --- /dev/null +++ b/test/scripts/fixtures/library_inherited2_sol_json_output.json @@ -0,0 +1,63 @@ +{ + "contracts": { + "syntaxTests/scoping/library_inherited2.sol": { + "A": { + "evm": { + "bytecode": { + "object": "6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea264697066735822122086e727f29d40b264a19bbfcad38d64493dca4bab5dbba8c82ffdaae389d2bba064736f6c63430008000033" + } + }, + "metadata": "{\"compiler\":{\"version\":\"0.8.0+commit.c7dfd78e\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"syntaxTests/scoping/library_inherited2.sol\":\"A\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"syntaxTests/scoping/library_inherited2.sol\":{\"keccak256\":\"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96\",\"urls\":[\"bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55\",\"dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet\"]}},\"version\":1}" + }, + "B": { + "evm": { + "bytecode": { + "object": "608060405234801561001057600080fd5b506101cc806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630423a13214610030575b600080fd5b61004a6004803603810190610045919061009d565b610060565b60405161005791906100d5565b60405180910390f35b600061006b82610072565b9050919050565b6000602a8261008191906100f0565b9050919050565b6000813590506100978161017f565b92915050565b6000602082840312156100af57600080fd5b60006100bd84828501610088565b91505092915050565b6100cf81610146565b82525050565b60006020820190506100ea60008301846100c6565b92915050565b60006100fb82610146565b915061010683610146565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0382111561013b5761013a610150565b5b828201905092915050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b61018881610146565b811461019357600080fd5b5056fea2646970667358221220104c345633313efe410492448844d96d78452c3044ce126b5e041b7fbeaa790064736f6c63430008000033" + } + }, + "metadata": "{\"compiler\":{\"version\":\"0.8.0+commit.c7dfd78e\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"bar\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"syntaxTests/scoping/library_inherited2.sol\":\"B\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"syntaxTests/scoping/library_inherited2.sol\":{\"keccak256\":\"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96\",\"urls\":[\"bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55\",\"dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet\"]}},\"version\":1}" + }, + "Lib": { + "evm": { + "bytecode": { + "object": "60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea26469706673582212207f9515e2263fa71a7984707e2aefd82241fac15c497386ca798b526f14f8ba6664736f6c63430008000033" + } + }, + "metadata": "{\"compiler\":{\"version\":\"0.8.0+commit.c7dfd78e\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"syntaxTests/scoping/library_inherited2.sol\":\"Lib\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"syntaxTests/scoping/library_inherited2.sol\":{\"keccak256\":\"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96\",\"urls\":[\"bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55\",\"dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet\"]}},\"version\":1}" + } + } + }, + "errors": [ + { + "component": "general", + "errorCode": "1878", + "formattedMessage": "Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing \"SPDX-License-Identifier: \" to each source file. Use \"SPDX-License-Identifier: UNLICENSED\" for non-open-source code. Please see https://spdx.org for more information.\n--> syntaxTests/scoping/library_inherited2.sol\n\n", + "message": "SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing \"SPDX-License-Identifier: \" to each source file. Use \"SPDX-License-Identifier: UNLICENSED\" for non-open-source code. Please see https://spdx.org for more information.", + "severity": "warning", + "sourceLocation": { + "end": -1, + "file": "syntaxTests/scoping/library_inherited2.sol", + "start": -1 + }, + "type": "Warning" + }, + { + "component": "general", + "errorCode": "3420", + "formattedMessage": "Warning: Source file does not specify required compiler version! Consider adding \"pragma solidity ^0.8.0;\"\n--> syntaxTests/scoping/library_inherited2.sol\n\n", + "message": "Source file does not specify required compiler version! Consider adding \"pragma solidity ^0.8.0;\"", + "severity": "warning", + "sourceLocation": { + "end": -1, + "file": "syntaxTests/scoping/library_inherited2.sol", + "start": -1 + }, + "type": "Warning" + } + ], + "sources": { + "syntaxTests/scoping/library_inherited2.sol": { + "id": 0 + } + } +} diff --git a/test/scripts/fixtures/unknown_pragma_sol_json_output.json b/test/scripts/fixtures/unknown_pragma_sol_json_output.json new file mode 100644 index 000000000..94d403984 --- /dev/null +++ b/test/scripts/fixtures/unknown_pragma_sol_json_output.json @@ -0,0 +1,44 @@ +{ + "errors": [ + { + "component": "general", + "errorCode": "1878", + "formattedMessage": "Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing \"SPDX-License-Identifier: \" to each source file. Use \"SPDX-License-Identifier: UNLICENSED\" for non-open-source code. Please see https://spdx.org for more information.\n--> syntaxTests/pragma/unknown_pragma.sol\n\n", + "message": "SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing \"SPDX-License-Identifier: \" to each source file. Use \"SPDX-License-Identifier: UNLICENSED\" for non-open-source code. Please see https://spdx.org for more information.", + "severity": "warning", + "sourceLocation": { + "end": -1, + "file": "syntaxTests/pragma/unknown_pragma.sol", + "start": -1 + }, + "type": "Warning" + }, + { + "component": "general", + "errorCode": "4936", + "formattedMessage": "SyntaxError: Unknown pragma \"thisdoesntexist\"\n --> syntaxTests/pragma/unknown_pragma.sol:1:1:\n |\n1 | pragma thisdoesntexist;\n | ^^^^^^^^^^^^^^^^^^^^^^^\n\n", + "message": "Unknown pragma \"thisdoesntexist\"", + "severity": "error", + "sourceLocation": { + "end": 23, + "file": "syntaxTests/pragma/unknown_pragma.sol", + "start": 0 + }, + "type": "SyntaxError" + }, + { + "component": "general", + "errorCode": "3420", + "formattedMessage": "Warning: Source file does not specify required compiler version! Consider adding \"pragma solidity ^0.8.0;\"\n--> syntaxTests/pragma/unknown_pragma.sol\n\n", + "message": "Source file does not specify required compiler version! Consider adding \"pragma solidity ^0.8.0;\"", + "severity": "warning", + "sourceLocation": { + "end": -1, + "file": "syntaxTests/pragma/unknown_pragma.sol", + "start": -1 + }, + "type": "Warning" + } + ], + "sources": {} +} diff --git a/test/scripts/test_bytecodecompare_prepare_report.py b/test/scripts/test_bytecodecompare_prepare_report.py new file mode 100644 index 000000000..8ec59c359 --- /dev/null +++ b/test/scripts/test_bytecodecompare_prepare_report.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python + +import json +import unittest +from pathlib import Path +from textwrap import dedent + +from unittest_helpers import LIBSOLIDITY_TEST_DIR, load_fixture, load_libsolidity_test_case + +# NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports +# pragma pylint: disable=import-error +from bytecodecompare.prepare_report import FileReport, ContractReport +from bytecodecompare.prepare_report import load_source, parse_standard_json_output, prepare_compiler_input +# pragma pylint: enable=import-error + + +SMT_SMOKE_TEST_SOL_PATH = LIBSOLIDITY_TEST_DIR / 'smtCheckerTests/simple/smoke_test.sol' +SMT_SMOKE_TEST_SOL_CODE = load_libsolidity_test_case(SMT_SMOKE_TEST_SOL_PATH) + +SYNTAX_SMOKE_TEST_SOL_PATH = LIBSOLIDITY_TEST_DIR / 'syntaxTests/smoke_test.sol' +SYNTAX_SMOKE_TEST_SOL_CODE = load_libsolidity_test_case(SYNTAX_SMOKE_TEST_SOL_PATH) + +LIBRARY_INHERITED2_SOL_JSON_OUTPUT = load_fixture('library_inherited2_sol_json_output.json') + +UNKNOWN_PRAGMA_SOL_JSON_OUTPUT = load_fixture('unknown_pragma_sol_json_output.json') + + +class TestPrepareReport_FileReport(unittest.TestCase): + def test_format_report(self): + report = FileReport( + file_name=Path('syntaxTests/scoping/library_inherited2.sol'), + contract_reports=[ + ContractReport( + contract_name='A', + file_name=Path('syntaxTests/smoke_test.sol'), + bytecode=None, + metadata=None, + ), + ContractReport( + contract_name='B', + file_name=Path('syntaxTests/smoke_test.sol'), + bytecode=None, + metadata='{"language":"Solidity"}', + ), + ContractReport( + contract_name='Lib', + file_name=Path('syntaxTests/smoke_test.sol'), + bytecode='60566050600b828282398051', + metadata=None, + ), + ] + ) + + expected_output = dedent("""\ + syntaxTests/scoping/library_inherited2.sol:A + syntaxTests/scoping/library_inherited2.sol:A + syntaxTests/scoping/library_inherited2.sol:B + syntaxTests/scoping/library_inherited2.sol:B {"language":"Solidity"} + syntaxTests/scoping/library_inherited2.sol:Lib 60566050600b828282398051 + syntaxTests/scoping/library_inherited2.sol:Lib + """) + + self.assertEqual(report.format_report(), expected_output) + + def test_format_report_should_print_error_if_contract_report_list_is_missing(self): + report = FileReport(file_name=Path('file.sol'), contract_reports=None) + + expected_output = dedent("""\ + file.sol: + """) + + self.assertEqual(report.format_report(), expected_output) + + def test_format_report_should_not_print_anything_if_contract_report_list_is_empty(self): + report = FileReport(file_name=Path('file.sol'), contract_reports=[]) + + self.assertEqual(report.format_report(), '') + + +class TestPrepareReport(unittest.TestCase): + def setUp(self): + self.maxDiff = 10000 + + def test_load_source(self): + self.assertEqual(load_source(SMT_SMOKE_TEST_SOL_PATH), SMT_SMOKE_TEST_SOL_CODE) + + def test_prepare_compiler_input(self): + expected_compiler_input = { + 'language': 'Solidity', + 'sources': { + str(SMT_SMOKE_TEST_SOL_PATH): {'content': SMT_SMOKE_TEST_SOL_CODE}, + }, + 'settings': { + 'optimizer': {'enabled': True}, + 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, + 'modelChecker': {'engine': 'none'}, + } + } + + (command_line, compiler_input) = prepare_compiler_input( + Path('solc'), + SMT_SMOKE_TEST_SOL_PATH, + optimize=True, + ) + + self.assertEqual(command_line, ['solc', '--standard-json']) + self.assertEqual(json.loads(compiler_input), expected_compiler_input) + + def test_parse_standard_json_output(self): + expected_report = FileReport( + file_name=Path('syntaxTests/scoping/library_inherited2.sol'), + contract_reports=[ + # pragma pylint: disable=line-too-long + ContractReport( + contract_name='A', + file_name=Path('syntaxTests/scoping/library_inherited2.sol'), + bytecode='6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea264697066735822122086e727f29d40b264a19bbfcad38d64493dca4bab5dbba8c82ffdaae389d2bba064736f6c63430008000033', + metadata='{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"A"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}', + ), + ContractReport( + contract_name='B', + file_name=Path('syntaxTests/scoping/library_inherited2.sol'), + bytecode='608060405234801561001057600080fd5b506101cc806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630423a13214610030575b600080fd5b61004a6004803603810190610045919061009d565b610060565b60405161005791906100d5565b60405180910390f35b600061006b82610072565b9050919050565b6000602a8261008191906100f0565b9050919050565b6000813590506100978161017f565b92915050565b6000602082840312156100af57600080fd5b60006100bd84828501610088565b91505092915050565b6100cf81610146565b82525050565b60006020820190506100ea60008301846100c6565b92915050565b60006100fb82610146565b915061010683610146565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0382111561013b5761013a610150565b5b828201905092915050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b61018881610146565b811461019357600080fd5b5056fea2646970667358221220104c345633313efe410492448844d96d78452c3044ce126b5e041b7fbeaa790064736f6c63430008000033', + metadata='{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"bar","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"B"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}', + ), + ContractReport( + contract_name='Lib', + file_name=Path('syntaxTests/scoping/library_inherited2.sol'), + bytecode='60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea26469706673582212207f9515e2263fa71a7984707e2aefd82241fac15c497386ca798b526f14f8ba6664736f6c63430008000033', + metadata='{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"Lib"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}', + ), + # pragma pylint: enable=line-too-long + ] + ) + + report = parse_standard_json_output( + Path('syntaxTests/scoping/library_inherited2.sol'), + LIBRARY_INHERITED2_SOL_JSON_OUTPUT, + ) + self.assertEqual(report, expected_report) + + def test_parse_standard_json_output_should_report_error_on_compiler_errors(self): + expected_report = FileReport(file_name=Path('syntaxTests/pragma/unknown_pragma.sol'), contract_reports=None) + + report = parse_standard_json_output(Path('syntaxTests/pragma/unknown_pragma.sol'), UNKNOWN_PRAGMA_SOL_JSON_OUTPUT) + self.assertEqual(report, expected_report) + + def test_parse_standard_json_output_should_report_error_on_empty_json(self): + expected_report = FileReport(file_name=Path('file.sol'), contract_reports=None) + + self.assertEqual(parse_standard_json_output(Path('file.sol'), '{}'), expected_report) + + def test_parse_standard_json_output_should_report_error_if_contracts_is_empty(self): + compiler_output = '{"contracts": {}}' + + expected_report = FileReport(file_name=Path('contract.sol'), contract_reports=None) + + self.assertEqual(parse_standard_json_output(Path('contract.sol'), compiler_output), expected_report) + + def test_parse_standard_json_output_should_report_error_if_every_file_has_no_contracts(self): + compiler_output = dedent("""\ + { + "contracts": { + "contract1.sol": {}, + "contract2.sol": {} + } + } + """) + + expected_report = FileReport(file_name=Path('contract.sol'), contract_reports=None) + + self.assertEqual(parse_standard_json_output(Path('contract.sol'), compiler_output), expected_report) + + def test_parse_standard_json_output_should_not_report_error_if_there_is_at_least_one_file_with_contracts(self): + compiler_output = dedent("""\ + { + "contracts": { + "contract1.sol": {"A": {}}, + "contract2.sol": {} + } + } + """) + + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ContractReport(contract_name='A', file_name=Path('contract1.sol'), bytecode=None, metadata=None)] + ) + + self.assertEqual(parse_standard_json_output(Path('contract.sol'), compiler_output), expected_report) diff --git a/test/scripts/unittest_helpers.py b/test/scripts/unittest_helpers.py new file mode 100644 index 000000000..957e9007a --- /dev/null +++ b/test/scripts/unittest_helpers.py @@ -0,0 +1,15 @@ +from pathlib import Path +from typing import Union + +LIBSOLIDITY_TEST_DIR = Path(__file__).parent.parent / 'libsolidity' +FIXTURE_DIR = Path(__file__).parent / 'fixtures' + +def load_file(path: Union[Path, str]) -> str: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + +def load_fixture(relative_path: Union[Path, str]) -> str: + return load_file(FIXTURE_DIR / relative_path) + +def load_libsolidity_test_case(relative_path: Union[Path, str]) -> str: + return load_file(LIBSOLIDITY_TEST_DIR / relative_path)