diff --git a/scripts/bytecodecompare/prepare_report.js b/scripts/bytecodecompare/prepare_report.js index 83cfc13e0..608337c86 100755 --- a/scripts/bytecodecompare/prepare_report.js +++ b/scripts/bytecodecompare/prepare_report.js @@ -4,45 +4,89 @@ const fs = require('fs') const compiler = require('./solc-js/wrapper.js')(require('./solc-js/soljson.js')) + +function loadSource(sourceFileName, stripSMTPragmas) +{ + source = fs.readFileSync(sourceFileName).toString() + + if (stripSMTPragmas) + // NOTE: replace() with string parameter replaces only the first occurrence. + return source.replace('pragma experimental SMTChecker;', ''); + + return source +} + +function cleanString(string) +{ + if (string !== undefined) + string = string.trim() + return (string !== '' ? string : undefined) +} + + +let stripSMTPragmas = false +let firstFileArgumentIndex = 2 + +if (process.argv.length >= 3 && process.argv[2] === '--strip-smt-pragmas') +{ + stripSMTPragmas = true + firstFileArgumentIndex = 3 +} + for (const optimize of [false, true]) { - for (const filename of process.argv.slice(2)) + for (const filename of process.argv.slice(firstFileArgumentIndex)) { if (filename !== undefined) { - const input = { + let input = { language: 'Solidity', sources: { - [filename]: {content: fs.readFileSync(filename).toString()} + [filename]: {content: loadSource(filename, stripSMTPragmas)} }, settings: { optimizer: {enabled: optimize}, - outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}}, - "modelChecker": {"engine": "none"} + outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}} } } + if (!stripSMTPragmas) + input['settings']['modelChecker'] = {engine: 'none'} - const result = JSON.parse(compiler.compile(JSON.stringify(input))) + let serializedOutput + let result + const serializedInput = JSON.stringify(input) let internalCompilerError = false - if ('errors' in result) + try { - for (const error of result['errors']) - // JSON interface still returns contract metadata in case of an internal compiler error while - // CLI interface does not. To make reports comparable we must force this case to be detected as - // an error in both cases. - if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type'])) - { - internalCompilerError = true - break - } + serializedOutput = compiler.compile(serializedInput) + } + catch (exception) + { + internalCompilerError = true + } + + if (!internalCompilerError) + { + result = JSON.parse(serializedOutput) + + if ('errors' in result) + for (const error of result['errors']) + // JSON interface still returns contract metadata in case of an internal compiler error while + // CLI interface does not. To make reports comparable we must force this case to be detected as + // an error in both cases. + if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type'])) + { + internalCompilerError = true + break + } } if ( + internalCompilerError || !('contracts' in result) || Object.keys(result['contracts']).length === 0 || - Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0) || - internalCompilerError + 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 + ': ') @@ -55,10 +99,15 @@ for (const optimize of [false, true]) let bytecode = '' let metadata = '' - if ('evm' in contractResults && 'bytecode' in contractResults['evm'] && 'object' in contractResults['evm']['bytecode']) - bytecode = contractResults.evm.bytecode.object + if ( + 'evm' in contractResults && + 'bytecode' in contractResults['evm'] && + 'object' in contractResults['evm']['bytecode'] && + cleanString(contractResults.evm.bytecode.object) !== undefined + ) + bytecode = cleanString(contractResults.evm.bytecode.object) - if ('metadata' in contractResults) + if ('metadata' in contractResults && cleanString(contractResults.metadata) !== undefined) metadata = contractResults.metadata console.log(filename + ':' + contractName + ' ' + bytecode) diff --git a/scripts/bytecodecompare/prepare_report.py b/scripts/bytecodecompare/prepare_report.py index 054aa18b5..f185a5840 100755 --- a/scripts/bytecodecompare/prepare_report.py +++ b/scripts/bytecodecompare/prepare_report.py @@ -13,9 +13,12 @@ from tempfile import TemporaryDirectory from typing import List, Optional, Tuple, Union -CONTRACT_SEPARATOR_PATTERN = re.compile(r'^======= (?P.+):(?P[^:]+) =======$', re.MULTILINE) -BYTECODE_REGEX = re.compile(r'^Binary:\n(?P.*)$', re.MULTILINE) -METADATA_REGEX = re.compile(r'^Metadata:\n(?P\{.*\})$', re.MULTILINE) +CONTRACT_SEPARATOR_PATTERN = re.compile( + r'^ *======= +(?:(?P.+) *:)? *(?P[^:]+) +======= *$', + re.MULTILINE +) +BYTECODE_REGEX = re.compile(r'^ *Binary: *\n(?P.*[0-9a-f$_]+.*)$', re.MULTILINE) +METADATA_REGEX = re.compile(r'^ *Metadata: *\n *(?P\{.*\}) *$', re.MULTILINE) class CompilerInterface(Enum): @@ -23,10 +26,16 @@ class CompilerInterface(Enum): STANDARD_JSON = 'standard-json' +class SMTUse(Enum): + PRESERVE = 'preserve' + DISABLE = 'disable' + STRIP_PRAGMAS = 'strip-pragmas' + + @dataclass(frozen=True) class ContractReport: contract_name: str - file_name: Path + file_name: Optional[Path] bytecode: Optional[str] metadata: Optional[str] @@ -54,15 +63,23 @@ class FileReport: return report -def load_source(path: Union[Path, str]) -> str: +def load_source(path: Union[Path, str], smt_use: SMTUse) -> str: # NOTE: newline='' disables newline conversion. # We want the file exactly as is because changing even a single byte in the source affects metadata. with open(path, mode='r', encoding='utf8', newline='') as source_file: file_content = source_file.read() + if smt_use == SMTUse.STRIP_PRAGMAS: + return file_content.replace('pragma experimental SMTChecker;', '', 1) + return file_content +def clean_string(value: Optional[str]) -> Optional[str]: + value = value.strip() if value is not None else None + return value if value != '' else None + + def parse_standard_json_output(source_file_name: Path, standard_json_output: str) -> FileReport: decoded_json_output = json.loads(standard_json_output.strip()) @@ -89,8 +106,8 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str 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'), + bytecode=clean_string(contract_results.get('evm', {}).get('bytecode', {}).get('object')), + metadata=clean_string(contract_results.get('metadata')), )) return file_report @@ -113,55 +130,92 @@ def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport: assert file_report.contract_reports is not None file_report.contract_reports.append(ContractReport( - contract_name=contract_name, - file_name=Path(file_name), - bytecode=bytecode_match['bytecode'] if bytecode_match is not None else None, - metadata=metadata_match['metadata'] if metadata_match is not None else None, + contract_name=contract_name.strip(), + file_name=Path(file_name.strip()) if file_name is not None else None, + bytecode=clean_string(bytecode_match['bytecode'] if bytecode_match is not None else None), + metadata=clean_string(metadata_match['metadata'] if metadata_match is not None else None), )) return file_report -def prepare_compiler_input( +def prepare_compiler_input( # pylint: disable=too-many-arguments compiler_path: Path, source_file_name: Path, optimize: bool, - interface: CompilerInterface + force_no_optimize_yul: bool, + interface: CompilerInterface, + smt_use: SMTUse, + metadata_option_supported: bool, ) -> Tuple[List[str], str]: if interface == CompilerInterface.STANDARD_JSON: json_input: dict = { 'language': 'Solidity', 'sources': { - str(source_file_name): {'content': load_source(source_file_name)} + str(source_file_name): {'content': load_source(source_file_name, smt_use)} }, 'settings': { 'optimizer': {'enabled': optimize}, 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, - 'modelChecker': {'engine': 'none'}, } } + if smt_use == SMTUse.DISABLE: + json_input['settings']['modelChecker'] = {'engine': 'none'} + command_line = [str(compiler_path), '--standard-json'] compiler_input = json.dumps(json_input) else: assert interface == CompilerInterface.CLI - compiler_options = [str(source_file_name), '--bin', '--metadata', '--model-checker-engine', 'none'] + compiler_options = [str(source_file_name), '--bin'] + if metadata_option_supported: + compiler_options.append('--metadata') if optimize: compiler_options.append('--optimize') + elif force_no_optimize_yul: + compiler_options.append('--no-optimize-yul') + if smt_use == SMTUse.DISABLE: + compiler_options += ['--model-checker-engine', 'none'] command_line = [str(compiler_path)] + compiler_options - compiler_input = load_source(source_file_name) + compiler_input = load_source(source_file_name, smt_use) return (command_line, compiler_input) -def run_compiler( +def detect_metadata_cli_option_support(compiler_path: Path): + process = subprocess.run( + [str(compiler_path.absolute()), '--metadata', '-'], + input="contract C {}", + encoding='utf8', + capture_output=True, + check=False, + ) + + negative_response = "unrecognised option '--metadata'".strip() + if (process.returncode == 0) != (process.stderr.strip() != negative_response): + # If the error is other than expected or there's an error message but no error, don't try + # to guess. Just fail. + print( + f"Compiler exit code: {process.returncode}\n" + f"Compiler output:\n{process.stderr}\n", + file=sys.stderr + ) + raise Exception("Failed to determine if the compiler supports the --metadata option.") + + return process.returncode == 0 + + +def run_compiler( # pylint: disable=too-many-arguments compiler_path: Path, source_file_name: Path, optimize: bool, + force_no_optimize_yul: bool, interface: CompilerInterface, + smt_use: SMTUse, + metadata_option_supported: bool, tmp_dir: Path, ) -> FileReport: @@ -170,7 +224,10 @@ def run_compiler( compiler_path, Path(source_file_name.name), optimize, - interface + force_no_optimize_yul, + interface, + smt_use, + metadata_option_supported, ) process = subprocess.run( @@ -190,7 +247,10 @@ def run_compiler( compiler_path.absolute(), Path(source_file_name.name), optimize, - interface + force_no_optimize_yul, + interface, + smt_use, + metadata_option_supported, ) # Create a copy that we can use directly with the CLI interface @@ -211,13 +271,30 @@ def run_compiler( return parse_cli_output(Path(source_file_name), process.stdout) -def generate_report(source_file_names: List[str], compiler_path: Path, interface: CompilerInterface): +def generate_report( + source_file_names: List[str], + compiler_path: Path, + interface: CompilerInterface, + smt_use: SMTUse, + force_no_optimize_yul: bool +): + metadata_option_supported = detect_metadata_cli_option_support(compiler_path) + with open('report.txt', mode='w', encoding='utf8', newline='\n') as report_file: for optimize in [False, True]: with TemporaryDirectory(prefix='prepare_report-') as tmp_dir: for source_file_name in sorted(source_file_names): try: - report = run_compiler(compiler_path, Path(source_file_name), optimize, interface, Path(tmp_dir)) + report = run_compiler( + compiler_path, + Path(source_file_name), + optimize, + force_no_optimize_yul, + interface, + smt_use, + metadata_option_supported, + Path(tmp_dir), + ) report_file.write(report.format_report()) except subprocess.CalledProcessError as exception: print( @@ -250,7 +327,21 @@ def commandline_parser() -> ArgumentParser: dest='interface', default=CompilerInterface.STANDARD_JSON.value, choices=[c.value for c in CompilerInterface], - help="Compiler interface to use." + help="Compiler interface to use.", + ) + parser.add_argument( + '--smt-use', + dest='smt_use', + default=SMTUse.DISABLE.value, + choices=[s.value for s in SMTUse], + help="What to do about contracts that use the experimental SMT checker." + ) + parser.add_argument( + '--force-no-optimize-yul', + dest='force_no_optimize_yul', + default=False, + action='store_true', + help="Explicitly disable Yul optimizer in CLI runs without optimization to work around a bug in solc 0.6.0 and 0.6.1." ) return parser; @@ -261,4 +352,6 @@ if __name__ == "__main__": glob("*.sol"), Path(options.compiler_path), CompilerInterface(options.interface), + SMTUse(options.smt_use), + options.force_no_optimize_yul, ) diff --git a/test/scripts/fixtures/solc_0.4.0_cli_output.txt b/test/scripts/fixtures/solc_0.4.0_cli_output.txt new file mode 100644 index 000000000..2000013c5 --- /dev/null +++ b/test/scripts/fixtures/solc_0.4.0_cli_output.txt @@ -0,0 +1,8 @@ +contract.sol:1:1: Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.4.0;". +contract C {} +^ +Spanning multiple lines. + +======= C ======= +Binary: +6060604052600c8060106000396000f360606040526008565b600256 diff --git a/test/scripts/fixtures/solc_0.4.8_cli_output.txt b/test/scripts/fixtures/solc_0.4.8_cli_output.txt new file mode 100644 index 000000000..619e5c39e --- /dev/null +++ b/test/scripts/fixtures/solc_0.4.8_cli_output.txt @@ -0,0 +1,10 @@ +contract.sol:1:1: Warning: Source file does not specify required compiler version!Consider adding "pragma solidity ^0.4.8 +contract C {} +^ +Spanning multiple lines. + +======= C ======= +Binary: +6060604052346000575b60358060166000396000f30060606040525b60005600a165627a7a72305820ccf9337430b4c4f7d6ad41efb10a94411a2af6a9f173ef52daeadd31f4bf11890029 +Metadata: +{"compiler":{"version":"0.4.8+commit.60cc1668.mod.Darwin.appleclang"},"language":"Solidity","output":{"abi":[],"devdoc":{"methods":{}},"userdoc":{"methods":{}}},"settings":{"compilationTarget":{"contract.sol":"C"},"libraries":{},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"contract.sol":{"keccak256":"0xbe86d3681a198587296ad6d4a834606197e1a8f8944922c501631b04e21eeba2","urls":["bzzr://af16957d3d86013309d64d3cc572d007b1d8b08a821f2ff366840deb54a78524"]}},"version":1} diff --git a/test/scripts/test_bytecodecompare_prepare_report.py b/test/scripts/test_bytecodecompare_prepare_report.py index 885548e9d..41e45de66 100644 --- a/test/scripts/test_bytecodecompare_prepare_report.py +++ b/test/scripts/test_bytecodecompare_prepare_report.py @@ -9,7 +9,7 @@ from unittest_helpers import FIXTURE_DIR, LIBSOLIDITY_TEST_DIR, load_fixture, lo # 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 CompilerInterface, FileReport, ContractReport +from bytecodecompare.prepare_report import CompilerInterface, FileReport, ContractReport, SMTUse from bytecodecompare.prepare_report import load_source, parse_cli_output, parse_standard_json_output, prepare_compiler_input # pragma pylint: enable=import-error @@ -41,6 +41,9 @@ STACK_TOO_DEEP_CLI_OUTPUT = load_fixture('stack_too_deep_cli_output.txt') CODE_GENERATION_ERROR_JSON_OUTPUT = load_fixture('code_generation_error_json_output.json') CODE_GENERATION_ERROR_CLI_OUTPUT = load_fixture('code_generation_error_cli_output.txt') +SOLC_0_4_0_CLI_OUTPUT = load_fixture('solc_0.4.0_cli_output.txt') +SOLC_0_4_8_CLI_OUTPUT = load_fixture('solc_0.4.8_cli_output.txt') + class PrepareReportTestBase(unittest.TestCase): def setUp(self): @@ -98,48 +101,58 @@ class TestFileReport(PrepareReportTestBase): class TestLoadSource(PrepareReportTestBase): - def test_load_source(self): - self.assertEqual(load_source(SMT_SMOKE_TEST_SOL_PATH), SMT_SMOKE_TEST_SOL_CODE) - - def test_load_source_preserves_lf_newlines(self): - expected_output = ( - "pragma experimental SMTChecker;\n" + def test_load_source_should_strip_smt_pragmas_if_requested(self): + expected_file_content = ( "\n" "contract C {\n" "}\n" ) - self.assertEqual(load_source(SMT_CONTRACT_WITH_LF_NEWLINES_SOL_PATH), expected_output) + self.assertEqual(load_source(SMT_SMOKE_TEST_SOL_PATH, SMTUse.STRIP_PRAGMAS), expected_file_content) + + def test_load_source_should_not_strip_smt_pragmas_if_not_requested(self): + self.assertEqual(load_source(SMT_SMOKE_TEST_SOL_PATH, SMTUse.DISABLE), SMT_SMOKE_TEST_SOL_CODE) + self.assertEqual(load_source(SMT_SMOKE_TEST_SOL_PATH, SMTUse.PRESERVE), SMT_SMOKE_TEST_SOL_CODE) + + def test_load_source_preserves_lf_newlines(self): + expected_output = ( + "\n" + "\n" + "contract C {\n" + "}\n" + ) + + self.assertEqual(load_source(SMT_CONTRACT_WITH_LF_NEWLINES_SOL_PATH, SMTUse.STRIP_PRAGMAS), expected_output) def test_load_source_preserves_crlf_newlines(self): expected_output = ( - "pragma experimental SMTChecker;\r\n" + "\r\n" "\r\n" "contract C {\r\n" "}\r\n" ) - self.assertEqual(load_source(SMT_CONTRACT_WITH_CRLF_NEWLINES_SOL_PATH), expected_output) + self.assertEqual(load_source(SMT_CONTRACT_WITH_CRLF_NEWLINES_SOL_PATH, SMTUse.STRIP_PRAGMAS), expected_output) def test_load_source_preserves_cr_newlines(self): expected_output = ( - "pragma experimental SMTChecker;\r" + "\r" "\r" "contract C {\r" "}\r" ) - self.assertEqual(load_source(SMT_CONTRACT_WITH_CR_NEWLINES_SOL_PATH), expected_output) + self.assertEqual(load_source(SMT_CONTRACT_WITH_CR_NEWLINES_SOL_PATH, SMTUse.STRIP_PRAGMAS), expected_output) def test_load_source_preserves_mixed_newlines(self): expected_output = ( - "pragma experimental SMTChecker;\n" + "\n" "\n" "contract C {\r" "}\r\n" ) - self.assertEqual(load_source(SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH), expected_output) + self.assertEqual(load_source(SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, SMTUse.STRIP_PRAGMAS), expected_output) class TestPrepareCompilerInput(PrepareReportTestBase): @@ -160,7 +173,10 @@ class TestPrepareCompilerInput(PrepareReportTestBase): Path('solc'), SMT_SMOKE_TEST_SOL_PATH, optimize=True, + force_no_optimize_yul=False, interface=CompilerInterface.STANDARD_JSON, + smt_use=SMTUse.DISABLE, + metadata_option_supported=True, ) self.assertEqual(command_line, ['solc', '--standard-json']) @@ -171,12 +187,15 @@ class TestPrepareCompilerInput(PrepareReportTestBase): Path('solc'), SMT_SMOKE_TEST_SOL_PATH, optimize=True, + force_no_optimize_yul=False, interface=CompilerInterface.CLI, + smt_use=SMTUse.DISABLE, + metadata_option_supported=True, ) self.assertEqual( command_line, - ['solc', str(SMT_SMOKE_TEST_SOL_PATH), '--bin', '--metadata', '--model-checker-engine', 'none', '--optimize'] + ['solc', str(SMT_SMOKE_TEST_SOL_PATH), '--bin', '--metadata', '--optimize', '--model-checker-engine', 'none'] ) self.assertEqual(compiler_input, SMT_SMOKE_TEST_SOL_CODE) @@ -203,7 +222,10 @@ class TestPrepareCompilerInput(PrepareReportTestBase): Path('solc'), SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, optimize=True, + force_no_optimize_yul=False, interface=CompilerInterface.STANDARD_JSON, + smt_use=SMTUse.DISABLE, + metadata_option_supported=True, ) self.assertEqual(command_line, ['solc', '--standard-json']) @@ -214,11 +236,48 @@ class TestPrepareCompilerInput(PrepareReportTestBase): Path('solc'), SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, optimize=True, + force_no_optimize_yul=True, interface=CompilerInterface.CLI, + smt_use=SMTUse.DISABLE, + metadata_option_supported=True, ) self.assertEqual(compiler_input, SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_CODE) + def test_prepare_compiler_input_for_cli_should_handle_force_no_optimize_yul_flag(self): + (command_line, compiler_input) = prepare_compiler_input( + Path('solc'), + SMT_SMOKE_TEST_SOL_PATH, + optimize=False, + force_no_optimize_yul=True, + interface=CompilerInterface.CLI, + smt_use=SMTUse.DISABLE, + metadata_option_supported=True, + ) + + self.assertEqual( + command_line, + ['solc', str(SMT_SMOKE_TEST_SOL_PATH), '--bin', '--metadata', '--no-optimize-yul', '--model-checker-engine', 'none'], + ) + self.assertEqual(compiler_input, SMT_SMOKE_TEST_SOL_CODE) + + def test_prepare_compiler_input_for_cli_should_not_use_metadata_option_if_not_supported(self): + (command_line, compiler_input) = prepare_compiler_input( + Path('solc'), + SMT_SMOKE_TEST_SOL_PATH, + optimize=True, + force_no_optimize_yul=False, + interface=CompilerInterface.CLI, + smt_use=SMTUse.PRESERVE, + metadata_option_supported=False, + ) + + self.assertEqual( + command_line, + ['solc', str(SMT_SMOKE_TEST_SOL_PATH), '--bin', '--optimize'], + ) + self.assertEqual(compiler_input, SMT_SMOKE_TEST_SOL_CODE) + class TestParseStandardJSONOutput(PrepareReportTestBase): def test_parse_standard_json_output(self): @@ -320,6 +379,34 @@ class TestParseStandardJSONOutput(PrepareReportTestBase): class TestParseCLIOutput(PrepareReportTestBase): + def test_parse_standard_json_output_should_report_missing_if_value_is_just_whitespace(self): + compiler_output = dedent("""\ + { + "contracts": { + "contract.sol": { + "A": { + "evm": {"bytecode": {"object": ""}}, + "metadata": "" + }, + "B": { + "evm": {"bytecode": {"object": " "}}, + "metadata": " " + } + } + } + } + """) + + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + ContractReport(contract_name='A', file_name=Path('contract.sol'), bytecode=None, metadata=None), + ContractReport(contract_name='B', file_name=Path('contract.sol'), bytecode=None, metadata=None), + ] + ) + + self.assertEqual(parse_standard_json_output(Path('contract.sol'), compiler_output), expected_report) + def test_parse_cli_output(self): expected_report = FileReport( file_name=Path('syntaxTests/scoping/library_inherited2.sol'), @@ -417,3 +504,109 @@ class TestParseCLIOutput(PrepareReportTestBase): expected_report = FileReport(file_name=Path('file.sol'), contract_reports=None) self.assertEqual(parse_cli_output(Path('file.sol'), CODE_GENERATION_ERROR_CLI_OUTPUT), expected_report) + + def test_parse_cli_output_should_handle_output_from_solc_0_4_0(self): + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + ContractReport( + contract_name='C', + file_name=None, + bytecode='6060604052600c8060106000396000f360606040526008565b600256', + metadata=None, + ) + ] + ) + + self.assertEqual(parse_cli_output(Path('contract.sol'), SOLC_0_4_0_CLI_OUTPUT), expected_report) + + def test_parse_cli_output_should_handle_output_from_solc_0_4_8(self): + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + # pragma pylint: disable=line-too-long + ContractReport( + contract_name='C', + file_name=None, + bytecode='6060604052346000575b60358060166000396000f30060606040525b60005600a165627a7a72305820ccf9337430b4c4f7d6ad41efb10a94411a2af6a9f173ef52daeadd31f4bf11890029', + metadata='{"compiler":{"version":"0.4.8+commit.60cc1668.mod.Darwin.appleclang"},"language":"Solidity","output":{"abi":[],"devdoc":{"methods":{}},"userdoc":{"methods":{}}},"settings":{"compilationTarget":{"contract.sol":"C"},"libraries":{},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"contract.sol":{"keccak256":"0xbe86d3681a198587296ad6d4a834606197e1a8f8944922c501631b04e21eeba2","urls":["bzzr://af16957d3d86013309d64d3cc572d007b1d8b08a821f2ff366840deb54a78524"]}},"version":1}', + ) + # pragma pylint: enable=line-too-long + ] + ) + + self.assertEqual(parse_cli_output(Path('contract.sol'), SOLC_0_4_8_CLI_OUTPUT), expected_report) + + def test_parse_cli_output_should_handle_leading_and_trailing_spaces(self): + compiler_output = ( + ' ======= contract.sol : C ======= \n' + ' Binary: \n' + ' 60806040523480156 \n' + ' Metadata: \n' + ' { } \n' + ) + + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + ContractReport(contract_name='C', file_name=Path('contract.sol'), bytecode='60806040523480156', metadata='{ }') + ] + ) + + self.assertEqual(parse_cli_output(Path('contract.sol'), compiler_output), expected_report) + + def test_parse_cli_output_should_handle_empty_bytecode_and_metadata_lines(self): + compiler_output = dedent("""\ + ======= contract.sol:C ======= + Binary: + 60806040523480156 + Metadata: + + + ======= contract.sol:D ======= + Binary: + + Metadata: + {} + + + ======= contract.sol:E ======= + Binary: + + Metadata: + + + """) + + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + ContractReport(contract_name='C', file_name=Path('contract.sol'), bytecode='60806040523480156', metadata=None), + ContractReport(contract_name='D', file_name=Path('contract.sol'), bytecode=None, metadata='{}'), + ContractReport(contract_name='E', file_name=Path('contract.sol'), bytecode=None, metadata=None), + ] + ) + + self.assertEqual(parse_cli_output(Path('contract.sol'), compiler_output), expected_report) + + def test_parse_cli_output_should_handle_link_references_in_bytecode(self): + compiler_output = dedent("""\ + ======= contract.sol:C ======= + Binary: + 73123456789012345678901234567890123456789073__$fb58009a6b1ecea3b9d99bedd645df4ec3$__5050 + ======= contract.sol:D ======= + Binary: + __$fb58009a6b1ecea3b9d99bedd645df4ec3$__ + """) + + # pragma pylint: disable=line-too-long + expected_report = FileReport( + file_name=Path('contract.sol'), + contract_reports=[ + ContractReport(contract_name='C', file_name=Path('contract.sol'), bytecode='73123456789012345678901234567890123456789073__$fb58009a6b1ecea3b9d99bedd645df4ec3$__5050', metadata=None), + ContractReport(contract_name='D', file_name=Path('contract.sol'), bytecode='__$fb58009a6b1ecea3b9d99bedd645df4ec3$__', metadata=None), + ] + ) + # pragma pylint: enable=line-too-long + + self.assertEqual(parse_cli_output(Path('contract.sol'), compiler_output), expected_report)