From c11e583c5eda0a3ce2874e4f480d10ab3cbb0191 Mon Sep 17 00:00:00 2001 From: antazoey Date: Mon, 13 Nov 2023 14:27:30 -0600 Subject: [PATCH] feat: use new settings and implement compile code [APE-1512] (#125) --- .pre-commit-config.yaml | 10 +- CONTRIBUTING.md | 9 -- ape_solidity/_utils.py | 108 +++++++++++------ ape_solidity/compiler.py | 237 +++++++++++++++++++++++++++++-------- ape_solidity/exceptions.py | 14 ++- setup.py | 12 +- tests/conftest.py | 27 ++--- tests/test_compiler.py | 22 +++- 8 files changed, 311 insertions(+), 128 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e309acb..96157e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: check-yaml @@ -10,24 +10,24 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.11.0 hooks: - id: black name: black - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.7.0 hooks: - id: mypy additional_dependencies: [types-requests, types-setuptools, pydantic, types-pkg-resources] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.14 + rev: 0.7.17 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e89bfd9..80aa768 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,12 +47,3 @@ If you are opening a work-in-progress pull request to verify that it passes CI t [marking it as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). Join the ApeWorX [Discord](https://discord.gg/apeworx) if you have any questions. - -## Testing - -By default, the test suite will use a new, temporary path for the Solidity compiler installations. -This ensures that the tests always run from a clean slate without any relying on existing installations. - -If you wish to use your existing `~/.solcx` installations instead, you must set the environment variable `APE_SOLIDITY_USE_SYSTEM_SOLC=1`. - -This will ensure that py-solc-x's default path will be used, but any compilers installed as part of the tests will not be removed after tests have completed. diff --git a/ape_solidity/_utils.py b/ape_solidity/_utils.py index 9c03891..237ec71 100644 --- a/ape_solidity/_utils.py +++ b/ape_solidity/_utils.py @@ -1,20 +1,19 @@ import json import os import re -import subprocess from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Sequence, Set, Union +from ape._pydantic_compat import BaseModel, validator from ape.exceptions import CompilerError from ape.logging import logger +from packaging.specifiers import SpecifierSet from packaging.version import InvalidVersion +from packaging.version import Version from packaging.version import Version as _Version -from pydantic import BaseModel, validator -from semantic_version import NpmSpec, Version # type: ignore -from solcx.exceptions import SolcError # type: ignore -from solcx.install import get_executable # type: ignore -from solcx.wrapper import VERSION_REGEX # type: ignore +from solcx.install import get_executable +from solcx.wrapper import get_solc_version as get_solc_version_from_binary from ape_solidity.exceptions import IncorrectMappingFormatError @@ -138,35 +137,64 @@ def get_import_lines(source_paths: Set[Path]) -> Dict[Path, List[str]]: return imports_dict -def get_pragma_spec(source_file_path: Path) -> Optional[NpmSpec]: +def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional[SpecifierSet]: """ Extracts pragma information from Solidity source code. + Args: - source_file_path: Solidity source code - Returns: NpmSpec object or None, if no valid pragma is found + source_file_path (Union[Path, str]): Solidity source file path. + + Returns: + ``packaging.specifiers.SpecifierSet`` """ - if not source_file_path.is_file(): + path = Path(source_file_path) + if not path.is_file(): return None - source = source_file_path.read_text() - pragma_match = next(re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source), None) - if pragma_match is None: + source_str = path.read_text() + return get_pragma_spec_from_str(source_str) + + +def get_pragma_spec_from_str(source_str: str) -> Optional[SpecifierSet]: + if not ( + pragma_match := next( + re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source_str), None + ) + ): return None # Try compiling with latest # The following logic handles the case where the user puts a space - # between the operator and the version number in the pragam string, + # between the operator and the version number in the pragma string, # such as `solidity >= 0.4.19 < 0.7.0`. - pragma_expression = "" pragma_parts = pragma_match.groups()[0].split() - num_parts = len(pragma_parts) - for index in range(num_parts): - pragma_expression += pragma_parts[index] - if any([c.isdigit() for c in pragma_parts[index]]) and index < num_parts - 1: - pragma_expression += " " - try: - return NpmSpec(pragma_expression) + def _to_spec(item: str) -> str: + item = item.replace("^", "~=") + if item and item[0].isnumeric(): + return f"=={item}" + elif item and len(item) >= 2 and item[0] == "=" and item[1] != "=": + return f"={item}" + + return item + + pragma_parts_fixed = [] + builder = "" + for sub_part in pragma_parts: + if not any(c.isnumeric() for c in sub_part): + # Handle pragma with spaces between constraint and values + # like `>= 0.6.0`. + builder += sub_part + continue + elif builder: + spec = _to_spec(f"{builder}{sub_part}") + builder = "" + else: + spec = _to_spec(sub_part) + pragma_parts_fixed.append(spec) + + try: + return SpecifierSet(",".join(pragma_parts_fixed)) except ValueError as err: logger.error(str(err)) return None @@ -176,21 +204,15 @@ def load_dict(data: Union[str, dict]) -> Dict: return data if isinstance(data, dict) else json.loads(data) -def get_version_with_commit_hash(version: Union[str, Version]) -> Version: - # Borrowed from: - # https://github.com/iamdefinitelyahuman/py-solc-x/blob/master/solcx/wrapper.py#L15-L28 - if "+commit" in str(version): - return Version(str(version)) +def add_commit_hash(version: Union[str, Version]) -> Version: + vers = Version(f"{version}") if isinstance(version, str) else version + has_commit = len(f"{vers}") > len(vers.base_version) + if has_commit: + # Already added. + return vers - executable = get_executable(version) - stdout_data = subprocess.check_output([str(executable), "--version"], encoding="utf8") - try: - match = next(re.finditer(VERSION_REGEX, stdout_data)) - version_str = "".join(match.groups()) - except StopIteration: - raise SolcError("Could not determine the solc binary version") - - return Version.coerce(version_str) + solc = get_executable(version=vers) + return get_solc_version_from_binary(solc, with_commit_hash=True) def verify_contract_filepaths(contract_filepaths: List[Path]) -> Set[Path]: @@ -200,3 +222,15 @@ def verify_contract_filepaths(contract_filepaths: List[Path]) -> Set[Path]: sources_str = "', '".join(invalid_files) raise CompilerError(f"Unable to compile '{sources_str}' using Solidity compiler.") + + +def select_version(pragma_spec: SpecifierSet, options: Sequence[Version]) -> Optional[Version]: + choices = sorted(list(pragma_spec.filter(options)), reverse=True) + return choices[0] if choices else None + + +def strip_commit_hash(version: Union[str, Version]) -> Version: + """ + Version('0.8.21+commit.d9974bed') => Version('0.8.21')> the simple way. + """ + return Version(f"{str(version).split('+')[0].strip()}") diff --git a/ape_solidity/compiler.py b/ape_solidity/compiler.py index 68e8333..5dab679 100644 --- a/ape_solidity/compiler.py +++ b/ape_solidity/compiler.py @@ -5,7 +5,7 @@ from ape.api import CompilerAPI, PluginConfig from ape.contracts import ContractInstance -from ape.exceptions import CompilerError, ContractLogicError +from ape.exceptions import CompilerError, ConfigError, ContractLogicError from ape.logging import logger from ape.types import AddressType, ContractType from ape.utils import cached_property, get_relative_path @@ -13,27 +13,32 @@ from ethpm_types import ASTNode, HexBytes, PackageManifest from ethpm_types.ast import ASTClassification from ethpm_types.source import Content +from packaging.specifiers import SpecifierSet +from packaging.version import Version from pkg_resources import get_distribution from requests.exceptions import ConnectionError -from semantic_version import NpmSpec, Version # type: ignore -from solcx import compile_standard # type: ignore -from solcx import ( # type: ignore +from solcx import ( + compile_source, + compile_standard, get_installable_solc_versions, get_installed_solc_versions, install_solc, ) -from solcx.exceptions import SolcError # type: ignore -from solcx.install import get_executable # type: ignore +from solcx.exceptions import SolcError +from solcx.install import get_executable from ape_solidity._utils import ( OUTPUT_SELECTION, Extension, ImportRemapping, ImportRemappingBuilder, + add_commit_hash, get_import_lines, - get_pragma_spec, - get_version_with_commit_hash, + get_pragma_spec_from_path, + get_pragma_spec_from_str, load_dict, + select_version, + strip_commit_hash, verify_contract_filepaths, ) from ape_solidity.exceptions import ( @@ -42,6 +47,7 @@ IncorrectMappingFormatError, RuntimeErrorType, RuntimeErrorUnion, + SolcInstallError, ) # Define a regex pattern that matches import statements @@ -96,8 +102,54 @@ def available_versions(self) -> List[Version]: @property def installed_versions(self) -> List[Version]: + """ + Returns a lis of installed version WITHOUT their + commit hashes. + """ return get_installed_solc_versions() + @property + def latest_version(self) -> Optional[Version]: + """ + Returns the latest version available of ``solc``. + When unable to retrieve available ``solc`` versions, such as + times disconnected from the Internet, returns ``None``. + """ + return _try_max(self.available_versions) + + @property + def latest_installed_version(self) -> Optional[Version]: + """ + Returns the highest version of all the installed versions. + If ``solc`` is not installed at all, returns ``None``. + """ + return _try_max(self.installed_versions) + + @property + def _settings_version(self) -> Optional[Version]: + """ + A helper property that gets, verifies, and installs (if needed) + the version specified in the settings. + """ + if not (version := self.settings.version): + return None + + installed_versions = self.installed_versions + specified_commit_hash = "+" in version + base_version = strip_commit_hash(version) + if base_version not in installed_versions: + install_solc(base_version, show_progress=True) + + settings_version = add_commit_hash(base_version) + if specified_commit_hash: + if settings_version != version: + raise ConfigError( + f"Commit hash from settings version {version} " + f"differs from installed: {settings_version}" + ) + + return settings_version + @cached_property def _ape_version(self) -> Version: return Version(get_distribution("eth-ape").version.split(".dev")[0].strip()) @@ -145,9 +197,9 @@ def get_versions(self, all_paths: List[Path]) -> Set[str]: versions = set() for path in all_paths: # Make sure we have the compiler available to compile this - version_spec = get_pragma_spec(path) - if version_spec: - versions.add(str(version_spec.select(self.available_versions))) + if version_spec := get_pragma_spec_from_path(path): + if selected_version := select_version(version_spec, self.available_versions): + versions.add(selected_version.base_version) return versions @@ -157,7 +209,7 @@ def get_import_remapping(self, base_path: Optional[Path] = None) -> Dict[str, st e.g. ``'@import_name=path/to/dependency'``. """ base_path = base_path or self.project_manager.contracts_folder - remappings = self.config.import_remapping + remappings = self.settings.import_remapping if not remappings: return {} @@ -180,12 +232,19 @@ def get_import_remapping(self, base_path: Optional[Path] = None) -> Dict[str, st # Download dependencies for first time. # This only happens if calling this method before compiling in ape core. - _ = self.project_manager.dependencies + dependencies = self.project_manager.dependencies for item in remappings: remapping_obj = ImportRemapping(entry=item, packages_cache=packages_cache) builder.add_entry(remapping_obj) package_id = remapping_obj.package_id + + # Handle missing version ID + if len(package_id.parts) == 1: + if package_id.name in dependencies and len(dependencies[package_id.name]) == 1: + version_id = next(iter(dependencies[package_id.name])) + package_id = package_id / version_id + data_folder_cache = packages_cache / package_id # Re-build a downloaded dependency manifest into the .cache directory for imports. @@ -315,7 +374,7 @@ def get_compiler_settings( settings: Dict = {} for solc_version, sources in files_by_solc_version.items(): version_settings: Dict[str, Union[Any, List[Any]]] = { - "optimizer": {"enabled": self.config.optimize, "runs": 200}, + "optimizer": {"enabled": self.settings.optimize, "runs": 200}, "outputSelection": { str(get_relative_path(p, base_path)): {"*": OUTPUT_SELECTION, "": ["ast"]} for p in sources @@ -341,12 +400,12 @@ def get_compiler_settings( # Standard JSON input requires remappings to be sorted. version_settings["remappings"] = sorted(list(remappings_used)) - evm_version = self.config.evm_version + evm_version = self.settings.evm_version if evm_version: version_settings["evmVersion"] = evm_version if solc_version >= Version("0.7.5"): - version_settings["viaIR"] = self.config.via_ir + version_settings["viaIR"] = self.settings.via_ir settings[solc_version] = version_settings @@ -365,13 +424,12 @@ def get_standard_input_json( settings = self.get_compiler_settings(contract_filepaths, base_path) input_jsons = {} for solc_version, vers_settings in settings.items(): - files = list(files_by_solc_version[solc_version]) - if not files: + if not list(files_by_solc_version[solc_version]): continue logger.debug(f"Compiling using Solidity compiler '{solc_version}'") - cleaned_version = solc_version.truncate() - solc_binary = get_executable(cleaned_version) + cleaned_version = Version(solc_version.base_version) + solc_binary = get_executable(version=cleaned_version) arguments = {"solc_binary": solc_binary, "solc_version": cleaned_version} if solc_version >= Version("0.6.9"): @@ -403,14 +461,17 @@ def compile( contract_types: List[ContractType] = [] input_jsons = self.get_standard_input_json(contract_filepaths, base_path=base_path) for solc_version, input_json in input_jsons.items(): - logger.debug(f"Compiling using Solidity compiler '{solc_version}'") - cleaned_version = solc_version.truncate() - solc_binary = get_executable(cleaned_version) - arguments = {"solc_binary": solc_binary, "solc_version": cleaned_version} + logger.info(f"Compiling using Solidity compiler '{solc_version}'.") + cleaned_version = Version(solc_version.base_version) + solc_binary = get_executable(version=cleaned_version) + arguments: Dict = {"solc_binary": solc_binary, "solc_version": cleaned_version} if solc_version >= Version("0.6.9"): arguments["base_path"] = base_path + # Allow empty contracts, like Vyper does. + arguments["allow_empty"] = True + try: output = compile_standard(input_json, **arguments) except SolcError as err: @@ -489,6 +550,62 @@ def classify_ast(_node: ASTNode): return contract_types + def compile_code( + self, + code: str, + base_path: Optional[Path] = None, + **kwargs, + ) -> ContractType: + if settings_version := self._settings_version: + version = settings_version + + elif pragma := self._get_pramga_spec_from_str(code): + if selected_version := select_version(pragma, self.installed_versions): + version = selected_version + else: + if selected_version := select_version(pragma, self.available_versions): + version = selected_version + install_solc(version, show_progress=False) + else: + raise SolcInstallError() + + elif latest_installed := self.latest_installed_version: + version = latest_installed + + elif latest := self.latest_version: + install_solc(latest, show_progress=False) + version = latest + + else: + raise SolcInstallError() + + version = add_commit_hash(version) + cleaned_version = Version(version.base_version) + executable = get_executable(cleaned_version) + try: + result = compile_source( + code, + import_remappings=self.get_import_remapping(base_path=base_path), + base_path=base_path, + solc_binary=executable, + solc_version=cleaned_version, + allow_empty=True, + ) + except SolcError as err: + raise CompilerError(str(err)) from err + + output = result[next(iter(result.keys()))] + return ContractType( + abi=output["abi"], + ast=output["ast"], + deploymentBytecode={"bytecode": HexBytes(output["bin"])}, + devdoc=load_dict(output["devdoc"]), + runtimeBytecode={"bytecode": HexBytes(output["bin-runtime"])}, + sourcemap=output["srcmap"], + userdoc=load_dict(output["userdoc"]), + **kwargs, + ) + def _get_unmapped_imports( self, contract_filepaths: List[Path], @@ -571,23 +688,22 @@ def get_version_map( source_paths_to_get.add(imported_source) # Use specified version if given one - if self.config.version is not None: - specified_version = Version(self.config.version) - if specified_version not in self.installed_versions: - install_solc(specified_version) - - specified_version_with_commit_hash = get_version_with_commit_hash(specified_version) - return {specified_version_with_commit_hash: source_paths_to_get} + if version := self._settings_version: + return {version: source_paths_to_get} # else: find best version per source file # Build map of pragma-specs. - source_by_pragma_spec = {p: self._get_pragma_spec(p) for p in source_paths_to_get} + source_by_pragma_spec = {p: get_pragma_spec_from_path(p) for p in source_paths_to_get} # If no Solidity version has been installed previously while fetching the # contract version pragma, we must install a compiler, so choose the latest - if not self.installed_versions and not any(source_by_pragma_spec.values()): - install_solc(max(self.available_versions), show_progress=False) + if ( + not self.installed_versions + and not any(source_by_pragma_spec.values()) + and (latest := self.latest_version) + ): + install_solc(latest, show_progress=False) # Adjust best-versions based on imports. files_by_solc_version: Dict[Version, Set[Path]] = {} @@ -604,8 +720,8 @@ def get_version_map( ) if imported_pragma_spec is not None and ( - imported_pragma_spec.expression.startswith("=") - or imported_pragma_spec.expression[0].isdigit() + str(imported_pragma_spec)[0].startswith("=") + or str(imported_pragma_spec)[0].isdigit() ): # Have to use this version. solc_version = imported_version @@ -645,7 +761,7 @@ def get_version_map( if not files_by_solc_version[solc_version]: del files_by_solc_version[solc_version] - return {get_version_with_commit_hash(v): ls for v, ls in files_by_solc_version.items()} + return {add_commit_hash(v): ls for v, ls in files_by_solc_version.items()} def _get_imported_source_paths( self, @@ -672,22 +788,21 @@ def _get_imported_source_paths( return return_set - def _get_pragma_spec(self, path: Path) -> Optional[NpmSpec]: - pragma_spec = get_pragma_spec(path) - if not pragma_spec: + def _get_pramga_spec_from_str(self, source_str: str) -> Optional[SpecifierSet]: + if not (pragma_spec := get_pragma_spec_from_str(source_str)): return None # Check if we need to install specified compiler version - if pragma_spec is pragma_spec.select(self.installed_versions): + if select_version(pragma_spec, self.installed_versions): return pragma_spec - compiler_version = pragma_spec.select(self.available_versions) - if compiler_version: + elif compiler_version := select_version(pragma_spec, self.available_versions): install_solc(compiler_version, show_progress=False) + else: # Attempt to use the best-installed version. for version in self.installed_versions: - if not pragma_spec.match(version): + if version not in pragma_spec: continue logger.warning( @@ -704,12 +819,30 @@ def _get_pragma_spec(self, path: Path) -> Optional[NpmSpec]: return pragma_spec def _get_best_version(self, path: Path, source_by_pragma_spec: Dict) -> Version: - pragma_spec = source_by_pragma_spec[path] - return ( - pragma_spec.select(self.installed_versions) - if pragma_spec - else max(self.installed_versions) - ) + compiler_version: Optional[Version] = None + if pragma_spec := source_by_pragma_spec.get(path): + if selected := select_version(pragma_spec, self.installed_versions): + compiler_version = selected + + elif selected := select_version(pragma_spec, self.available_versions): + # Install missing version. + # NOTE: Must be installed before adding commit hash. + install_solc(selected, show_progress=True) + compiler_version = add_commit_hash(selected) + + elif latest_installed := self.latest_installed_version: + compiler_version = latest_installed + + elif latest := self.latest_version: + # Download latest version. + install_solc(latest, show_progress=True) + compiler_version = latest + + else: + raise SolcInstallError() + + assert compiler_version # For mypy + return add_commit_hash(compiler_version) def enrich_error(self, err: ContractLogicError) -> ContractLogicError: if not is_0x_prefixed(err.revert_message): @@ -913,3 +1046,7 @@ def _import_str_to_source_id( index += 1 return source_id_value + + +def _try_max(ls: List[Any]): + return max(ls) if ls else None diff --git a/ape_solidity/exceptions.py b/ape_solidity/exceptions.py index 2a44c50..e3c617d 100644 --- a/ape_solidity/exceptions.py +++ b/ape_solidity/exceptions.py @@ -1,7 +1,19 @@ from enum import IntEnum from typing import Dict, Type, Union -from ape.exceptions import ConfigError, ContractLogicError +from ape.exceptions import CompilerError, ConfigError, ContractLogicError + + +class SolcInstallError(CompilerError): + """ + Raised when `solc` is not installed + and unable to be installed. + """ + + def __init__(self): + super().__init__( + "No versions of `solc` installed and unable to install latest `solc` version." + ) class IncorrectMappingFormatError(ConfigError, ValueError): diff --git a/setup.py b/setup.py index 4fb919f..d7e3067 100644 --- a/setup.py +++ b/setup.py @@ -11,14 +11,14 @@ "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer ], "lint": [ - "black>=23.3.0,<24", # Auto-formatter and linter - "mypy>=0.991,<1", # Static type analyzer + "black>=23.11.0,<24", # Auto-formatter and linter + "mypy>=1.7.0,<2", # Static type analyzer "types-requests", # Needed due to mypy typeshed "types-setuptools", # Needed due to mypy typeshed "types-pkg-resources", # Needed for type checking tests - "flake8>=6.0.0,<7", # Style linter + "flake8>=6.1.0,<7", # Style linter "isort>=5.10.1,<6", # Import sorting linter - "mdformat>=0.7.16", # Auto-formatter for markdown + "mdformat>=0.7.17", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates "pydantic<2.0", # Needed for successful type check. TODO: Remove after full v2 support. @@ -67,8 +67,8 @@ url="https://github.com/ApeWorX/ape-solidity", include_package_data=True, install_requires=[ - "py-solc-x>=1.1.0,<2", - "eth-ape>=0.6.12,<0.7", + "py-solc-x>=2.0.2,<3", + "eth-ape>=0.6.25,<0.7", "ethpm-types", # Use the version ape requires "packaging", # Use the version ape requires "requests", diff --git a/tests/conftest.py b/tests/conftest.py index d40004b..224f852 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import os import shutil from contextlib import contextmanager from distutils.dir_util import copy_tree @@ -33,26 +32,16 @@ def _tmp_solcx_path(monkeypatch): shutil.rmtree(solcx_install_path, ignore_errors=True) -@pytest.fixture( - scope="session", - autouse=os.environ.get("APE_SOLIDITY_USE_SYSTEM_SOLC") is None, -) -def setup_session_solcx_path(request): +@pytest.fixture +def fake_no_installs(mocker): """ - Creates a new, temporary installation path for solcx when the test suite is - run. - - This ensures the Solidity installations do not conflict with the user's - installed versions and that the installations from the tests are cleaned up - after the suite is finished. + Tricks the tests into thinking there are no installed versions. + This saves time because it won't actually need to install solc, + and it should still work. """ - from _pytest.monkeypatch import MonkeyPatch - - patch = MonkeyPatch() - request.addfinalizer(patch.undo) - - with _tmp_solcx_path(patch) as path: - yield path + patch = mocker.patch("ape_solidity.compiler.get_installed_solc_versions") + patch.return_value = [] + return patch @pytest.fixture diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 6b67e8c..d05e558 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -9,9 +9,9 @@ from ape.contracts import ContractContainer from ape.exceptions import CompilerError from ethpm_types.ast import ASTClassification +from packaging.version import Version from pkg_resources import get_distribution from requests.exceptions import ConnectionError -from semantic_version import Version # type: ignore from ape_solidity import Extension from ape_solidity._utils import OUTPUT_SELECTION @@ -50,6 +50,10 @@ def test_compile(project, contract): assert contract.source_id == f"{contract.name}.sol" +def test_compile_solc_not_installed(project, fake_no_installs): + assert len(project.load_contracts(use_cache=False)) > 0 + + def test_compile_when_offline(project, compiler, mocker): # When offline, getting solc versions raises a requests connection error. # This should trigger the plugin to return an empty list. @@ -525,3 +529,19 @@ def test_flatten(project, compiler, data_folder): flattened_source = compiler.flatten_contract(source_path) flattened_source_path = data_folder / "ImportingLessConstrainedVersionFlat.sol" assert str(flattened_source) == str(flattened_source_path.read_text()) + + +def test_compile_code(compiler): + code = """ +contract Contract { + function snakes() pure public returns(bool) { + return true; + } +} +""" + actual = compiler.compile_code(code, contractName="TestContractName") + assert actual.name == "TestContractName" + assert len(actual.abi) > 0 + assert actual.ast is not None + assert len(actual.runtime_bytecode.bytecode) > 0 + assert len(actual.deployment_bytecode.bytecode) > 0