├── .bumpversion.cfg ├── .circleci ├── config.yml └── merge_pr.sh ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .project-template ├── fill_template_vars.sh ├── refill_template_vars.sh └── template_vars.txt ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── _static │ └── .suppress-sphinx-build-warning ├── conf.py ├── index.rst ├── overview.rst └── releases.rst ├── pytest.ini ├── pytest_ethereum ├── __init__.py ├── _utils │ ├── __init__.py │ ├── abi.py │ └── linker.py ├── deployer.py ├── exceptions.py ├── linker.py ├── plugins.py ├── testing.py └── typing.py ├── setup.py ├── tests ├── conftest.py ├── core │ ├── test_deployer.py │ ├── test_import.py │ ├── test_linker.py │ ├── test_log.py │ ├── test_w3_fixture.py │ └── utils │ │ └── test_linker_utils.py └── manifests │ ├── greeter │ └── 1.0.0.json │ ├── ping │ ├── 1.0.0.json │ └── contracts │ │ └── ping.vy │ └── registry │ └── 1.0.0.json └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.3-alpha.7 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[^.]*)\.(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{stage}.{devnum} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:part:stage] 11 | optional_value = stable 12 | first_value = stable 13 | values = 14 | alpha 15 | beta 16 | stable 17 | 18 | [bumpversion:part:devnum] 19 | 20 | [bumpversion:file:setup.py] 21 | search = version='{current_version}', 22 | replace = version='{new_version}', 23 | 24 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | # heavily inspired by https://raw.githubusercontent.com/pinax/pinax-wiki/6bd2a99ab6f702e300d708532a6d1d9aa638b9f8/.circleci/config.yml 4 | 5 | common: &common 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - run: 10 | name: merge pull request base 11 | command: ./.circleci/merge_pr.sh 12 | - run: 13 | name: merge pull request base (2nd try) 14 | command: ./.circleci/merge_pr.sh 15 | when: on_fail 16 | - run: 17 | name: merge pull request base (3nd try) 18 | command: ./.circleci/merge_pr.sh 19 | when: on_fail 20 | - restore_cache: 21 | keys: 22 | - cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 23 | - run: 24 | name: install dependencies 25 | command: pip install --user tox 26 | - run: 27 | name: run tox 28 | command: ~/.local/bin/tox -r 29 | - save_cache: 30 | paths: 31 | - .hypothesis 32 | - .tox 33 | - ~/.cache/pip 34 | - ~/.local 35 | - ./eggs 36 | key: cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 37 | 38 | jobs: 39 | doctest: 40 | <<: *common 41 | docker: 42 | - image: circleci/python:3.6 43 | environment: 44 | TOXENV: doctest 45 | lint: 46 | <<: *common 47 | docker: 48 | - image: circleci/python:3.6 49 | environment: 50 | TOXENV: lint 51 | py36-core: 52 | <<: *common 53 | docker: 54 | - image: circleci/python:3.6 55 | environment: 56 | TOXENV: py36-core 57 | workflows: 58 | version: 2 59 | test: 60 | jobs: 61 | - doctest 62 | - lint 63 | - py36-core 64 | -------------------------------------------------------------------------------- /.circleci/merge_pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then 4 | PR_INFO_URL=https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER 5 | PR_BASE_BRANCH=$(curl -L "$PR_INFO_URL" | python -c 'import json, sys; obj = json.load(sys.stdin); sys.stdout.write(obj["base"]["ref"])') 6 | git fetch origin +"$PR_BASE_BRANCH":circleci/pr-base 7 | # We need these config values or git complains when creating the 8 | # merge commit 9 | git config --global user.name "Circle CI" 10 | git config --global user.email "circleci@example.com" 11 | git merge --no-edit circleci/pr-base 12 | fi 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _If this is a bug report, please fill in the following sections. 2 | If this is a feature request, delete and describe what you would like with examples._ 3 | 4 | ## What was wrong? 5 | 6 | ### Code that produced the error 7 | 8 | ```py 9 | CODE_TO_REPRODUCE 10 | ``` 11 | 12 | ### Full error output 13 | 14 | ```sh 15 | ERROR_HERE 16 | ``` 17 | 18 | ### Expected Result 19 | 20 | _This section may be deleted if the expectation is "don't crash"._ 21 | 22 | ```sh 23 | EXPECTED_RESULT 24 | ``` 25 | 26 | ### Environment 27 | 28 | ```sh 29 | # run this: 30 | $ python -m eth_utils 31 | 32 | # then copy the output here: 33 | OUTPUT_HERE 34 | ``` 35 | 36 | ## How can it be fixed? 37 | 38 | Fill this section in if you know how this could or should be fixed. 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What was wrong? 2 | 3 | Issue # 4 | 5 | ## How was it fixed? 6 | 7 | Summary of approach. 8 | 9 | #### Cute Animal Picture 10 | 11 | ![put a cute animal picture link inside the parentheses]() 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | venv* 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Complexity 40 | output/*.html 41 | output/*/index.html 42 | 43 | # Sphinx 44 | docs/_build 45 | docs/modules.rst 46 | docs/*.internal.rst 47 | docs/*.utils.rst 48 | docs/*._utils.* 49 | 50 | # Blockchain 51 | chains 52 | 53 | # Hypothese Property base testing 54 | .hypothesis 55 | 56 | # tox/pytest cache 57 | .cache 58 | 59 | # Test output logs 60 | logs 61 | ### JetBrains template 62 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 63 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 64 | 65 | # User-specific stuff: 66 | .idea/workspace.xml 67 | .idea/tasks.xml 68 | .idea/dictionaries 69 | .idea/vcs.xml 70 | .idea/jsLibraryMappings.xml 71 | 72 | # Sensitive or high-churn files: 73 | .idea/dataSources.ids 74 | .idea/dataSources.xml 75 | .idea/dataSources.local.xml 76 | .idea/sqlDataSources.xml 77 | .idea/dynamic.xml 78 | .idea/uiDesigner.xml 79 | 80 | # Gradle: 81 | .idea/gradle.xml 82 | .idea/libraries 83 | 84 | # Mongo Explorer plugin: 85 | .idea/mongoSettings.xml 86 | 87 | # VIM temp files 88 | *.swp 89 | 90 | ## File-based project format: 91 | *.iws 92 | 93 | ## Plugin-specific files: 94 | 95 | # IntelliJ 96 | /out/ 97 | 98 | # mpeltonen/sbt-idea plugin 99 | .idea_modules/ 100 | 101 | # JIRA plugin 102 | atlassian-ide-plugin.xml 103 | 104 | # Crashlytics plugin (for Android Studio and IntelliJ) 105 | com_crashlytics_export_strings.xml 106 | crashlytics.properties 107 | crashlytics-build.properties 108 | fabric.properties 109 | 110 | -------------------------------------------------------------------------------- /.project-template/fill_template_vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | PROJECT_ROOT=$(dirname $(dirname $(python -c 'import os, sys; sys.stdout.write(os.path.realpath(sys.argv[1]))' "$0"))) 8 | 9 | echo "What is your python module name?" 10 | read MODULE_NAME 11 | 12 | echo "What is your pypi package name? (default: $MODULE_NAME)" 13 | read PYPI_INPUT 14 | PYPI_NAME=${PYPI_INPUT:-$MODULE_NAME} 15 | 16 | echo "What is your github project name? (default: $PYPI_NAME)" 17 | read REPO_INPUT 18 | REPO_NAME=${REPO_INPUT:-$PYPI_NAME} 19 | 20 | echo "What is your readthedocs.org project name? (default: $PYPI_NAME)" 21 | read RTD_INPUT 22 | RTD_NAME=${RTD_INPUT:-$PYPI_NAME} 23 | 24 | echo "What is your project name (ex: at the top of the README)? (default: $REPO_NAME)" 25 | read PROJECT_INPUT 26 | PROJECT_NAME=${PROJECT_INPUT:-$REPO_NAME} 27 | 28 | echo "What is a one-liner describing the project?" 29 | read SHORT_DESCRIPTION 30 | 31 | _replace() { 32 | local find_cmd=(find "$PROJECT_ROOT" ! -perm -u=x ! -path '*/.git/*' -type f) 33 | 34 | if [[ $(uname) == Darwin ]]; then 35 | "${find_cmd[@]}" -exec sed -i '' "$1" {} + 36 | else 37 | "${find_cmd[@]}" -exec sed -i "$1" {} + 38 | fi 39 | } 40 | _replace "s//$MODULE_NAME/g" 41 | _replace "s//$PYPI_NAME/g" 42 | _replace "s//$REPO_NAME/g" 43 | _replace "s//$RTD_NAME/g" 44 | _replace "s//$PROJECT_NAME/g" 45 | _replace "s//$SHORT_DESCRIPTION/g" 46 | 47 | mkdir -p "$PROJECT_ROOT/$MODULE_NAME" 48 | touch "$PROJECT_ROOT/$MODULE_NAME/__init__.py" 49 | -------------------------------------------------------------------------------- /.project-template/refill_template_vars.sh: -------------------------------------------------------------------------------- 1 | TEMPLATE_DIR=$(dirname $(readlink -f "$0")) 2 | <"$TEMPLATE_DIR/template_vars.txt" "$TEMPLATE_DIR/fill_template_vars.sh" 3 | -------------------------------------------------------------------------------- /.project-template/template_vars.txt: -------------------------------------------------------------------------------- 1 | pytest_ethereum 2 | pytest-ethereum 3 | pytest-ethereum 4 | pytest-ethereum 5 | pytest-ethereum 6 | Pytest library for ethereum projects. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jason Carver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_SIGN_SETTING := $(shell git config commit.gpgSign) 2 | 3 | .PHONY: clean-pyc clean-build docs 4 | 5 | help: 6 | @echo "clean-build - remove build artifacts" 7 | @echo "clean-pyc - remove Python file artifacts" 8 | @echo "lint - check style with flake8" 9 | @echo "test - run tests quickly with the default Python" 10 | @echo "testall - run tests on every Python version with tox" 11 | @echo "release - package and upload a release" 12 | @echo "dist - package" 13 | 14 | clean: clean-build clean-pyc 15 | 16 | clean-build: 17 | rm -fr build/ 18 | rm -fr dist/ 19 | rm -fr *.egg-info 20 | 21 | clean-pyc: 22 | find . -name '*.pyc' -exec rm -f {} + 23 | find . -name '*.pyo' -exec rm -f {} + 24 | find . -name '*~' -exec rm -f {} + 25 | 26 | lint: 27 | tox -elint 28 | 29 | lint-roll: 30 | isort --recursive pytest_ethereum tests 31 | $(MAKE) lint 32 | 33 | test: 34 | pytest tests 35 | 36 | test-all: 37 | tox 38 | 39 | build-docs: 40 | sphinx-apidoc -o docs/ . setup.py "*conftest*" 41 | $(MAKE) -C docs clean 42 | $(MAKE) -C docs html 43 | $(MAKE) -C docs doctest 44 | 45 | docs: build-docs 46 | open docs/_build/html/index.html 47 | 48 | linux-docs: build-docs 49 | xdg-open docs/_build/html/index.html 50 | 51 | release: clean 52 | git config commit.gpgSign true 53 | bumpversion $(bump) 54 | git push upstream && git push upstream --tags 55 | python setup.py sdist bdist_wheel 56 | twine upload dist/* 57 | git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" 58 | 59 | dist: clean 60 | python setup.py sdist bdist_wheel 61 | ls -l dist 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-ethereum 2 | 3 | [![Join the chat at https://gitter.im/ethereum/pytest-ethereum](https://badges.gitter.im/ethereum/pytest-ethereum.svg)](https://gitter.im/ethereum/pytest-ethereum?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://circleci.com/gh/ethereum/pytest-ethereum.svg?style=shield)](https://circleci.com/gh/ethereum/pytest-ethereum) 5 | [![PyPI version](https://badge.fury.io/py/pytest-ethereum.svg)](https://badge.fury.io/py/pytest-ethereum) 6 | [![Python versions](https://img.shields.io/pypi/pyversions/pytest-ethereum.svg)](https://pypi.python.org/pypi/pytest-ethereum) 7 | [![Docs build](https://readthedocs.org/projects/pytest-ethereum/badge/?version=latest)](http://pytest-ethereum.readthedocs.io/en/latest/?badge=latest) 8 | 9 | 10 | Pytest library for ethereum projects. 11 | 12 | Read more in the [documentation on ReadTheDocs](https://pytest-ethereum.readthedocs.io/). [View the change log](https://pytest-ethereum.readthedocs.io/en/latest/releases.html). 13 | 14 | ## Quickstart 15 | 16 | ```sh 17 | pip install pytest-ethereum 18 | ``` 19 | 20 | ## Developer Setup 21 | 22 | If you would like to hack on pytest-ethereum, please check out the 23 | [Ethereum Development Tactical Manual](https://github.com/pipermerriam/ethereum-dev-tactical-manual) 24 | for information on how we do: 25 | 26 | - Testing 27 | - Pull Requests 28 | - Code Style 29 | - Documentation 30 | 31 | ### Development Environment Setup 32 | 33 | You can set up your dev environment with: 34 | 35 | ```sh 36 | git clone git@github.com:ethereum/pytest-ethereum.git 37 | cd pytest-ethereum 38 | virtualenv -p python3 venv 39 | . venv/bin/activate 40 | pip install -e .[dev] 41 | ``` 42 | 43 | ### Testing Setup 44 | 45 | During development, you might like to have tests run on every file save. 46 | 47 | Show flake8 errors on file change: 48 | 49 | ```sh 50 | # Test flake8 51 | when-changed -v -s -r -1 pytest_ethereum/ tests/ -c "clear; flake8 pytest_ethereum tests && echo 'flake8 success' || echo 'error'" 52 | ``` 53 | 54 | Run multi-process tests in one command, but without color: 55 | 56 | ```sh 57 | # in the project root: 58 | pytest --numprocesses=4 --looponfail --maxfail=1 59 | # the same thing, succinctly: 60 | pytest -n 4 -f --maxfail=1 61 | ``` 62 | 63 | Run in one thread, with color and desktop notifications: 64 | 65 | ```sh 66 | cd venv 67 | ptw --onfail "notify-send -t 5000 'Test failure ⚠⚠⚠⚠⚠' 'python 3 test on pytest-ethereum failed'" ../tests ../pytest_ethereum 68 | ``` 69 | 70 | ### Release setup 71 | 72 | For Debian-like systems: 73 | ``` 74 | apt install pandoc 75 | ``` 76 | 77 | To release a new version: 78 | 79 | ```sh 80 | make release bump=$$VERSION_PART_TO_BUMP$$ 81 | ``` 82 | 83 | #### How to bumpversion 84 | 85 | The version format for this repo is `{major}.{minor}.{patch}` for stable, and 86 | `{major}.{minor}.{patch}-{stage}.{devnum}` for unstable (`stage` can be alpha or beta). 87 | 88 | To issue the next version in line, specify which part to bump, 89 | like `make release bump=minor` or `make release bump=devnum`. 90 | 91 | If you are in a beta version, `make release bump=stage` will switch to a stable. 92 | 93 | To issue an unstable version when the current version is stable, specify the 94 | new version explicitly, like `make release bump="--new-version 4.0.0-alpha.1 devnum"` 95 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/web3.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/web3.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/web3" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/web3" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/.suppress-sphinx-build-warning: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/pytest-ethereum/5e45ded06469d5ca4d3b6b1d2b78ac6c7e7f9174/docs/_static/.suppress-sphinx-build-warning -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pytest-ethereum documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 16 20:43:24 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | #sys.path.insert(0, os.path.abspath('.')) 19 | 20 | import os 21 | 22 | DIR = os.path.dirname('__file__') 23 | with open (os.path.join(DIR, '../setup.py'), 'r') as f: 24 | for line in f: 25 | if 'version=' in line: 26 | setup_version = line.split('\'')[1] 27 | break 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | #needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | 'sphinx.ext.autodoc', 39 | 'sphinx.ext.doctest', 40 | 'sphinx.ext.intersphinx', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix of source filenames. 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'pytest-ethereum' 57 | copyright = '2018, Jason Carver, Piper Merriam' 58 | 59 | __version__ = setup_version 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = '.'.join(__version__.split('.')[:2]) 66 | # The full version, including alpha/beta/rc tags. 67 | release = __version__ 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | #language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | #today = '' 76 | # Else, today_fmt is used as the format for a strftime call. 77 | #today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | exclude_patterns = [ 82 | '_build', 83 | 'modules.rst', 84 | ] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output ---------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = 'sphinx_rtd_theme' 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | #html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Output file base name for HTML help builder. 192 | htmlhelp_basename = 'pytest_ethereumdoc' 193 | 194 | 195 | # -- Options for LaTeX output --------------------------------------------- 196 | 197 | latex_elements = { 198 | # The paper size ('letterpaper' or 'a4paper'). 199 | #'papersize': 'letterpaper', 200 | 201 | # The font size ('10pt', '11pt' or '12pt'). 202 | #'pointsize': '10pt', 203 | 204 | # Additional stuff for the LaTeX preamble. 205 | #'preamble': '', 206 | } 207 | 208 | # Grouping the document tree into LaTeX files. List of tuples 209 | # (source start file, target name, title, 210 | # author, documentclass [howto, manual, or own class]). 211 | latex_documents = [ 212 | ('index', 'pytest_ethereum.tex', 'pytest-ethereum Documentation', 213 | 'Jason Carver', 'manual'), 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at the top of 217 | # the title page. 218 | #latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings are parts, 221 | # not chapters. 222 | #latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | #latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | #latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | #latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | #latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output --------------------------------------- 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [ 242 | ('index', 'pytest_ethereum', 'pytest-ethereum Documentation', 243 | ['Jason Carver'], 1) 244 | ] 245 | 246 | # If true, show URL addresses after external links. 247 | #man_show_urls = False 248 | 249 | 250 | # -- Options for Texinfo output ------------------------------------------- 251 | 252 | # Grouping the document tree into Texinfo files. List of tuples 253 | # (source start file, target name, title, author, 254 | # dir menu entry, description, category) 255 | texinfo_documents = [ 256 | ('index', 'pytest-ethereum', 'pytest-ethereum Documentation', 257 | 'Jason Carver', 'pytest-ethereum', 'Pytest library for ethereum projects.', 258 | 'Miscellaneous'), 259 | ] 260 | 261 | # Documents to append as an appendix to all manuals. 262 | #texinfo_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | #texinfo_domain_indices = True 266 | 267 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 268 | #texinfo_show_urls = 'footnote' 269 | 270 | # If true, do not generate a @detailmenu in the "Top" node's menu. 271 | #texinfo_no_detailmenu = False 272 | 273 | # -- Intersphinx configuration ------------------------------------------------ 274 | 275 | intersphinx_mapping = { 276 | 'python': ('https://docs.python.org/3.5', None), 277 | } 278 | 279 | # -- Doctest configuration ---------------------------------------- 280 | 281 | import doctest 282 | 283 | doctest_default_flags = (0 284 | | doctest.DONT_ACCEPT_TRUE_FOR_1 285 | | doctest.ELLIPSIS 286 | | doctest.IGNORE_EXCEPTION_DETAIL 287 | | doctest.NORMALIZE_WHITESPACE 288 | ) 289 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pytest-ethereum 2 | ============================== 3 | 4 | Pytest library for ethereum projects. 5 | 6 | Contents 7 | -------- 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | 12 | overview 13 | releases 14 | 15 | 16 | Indices and tables 17 | ------------------ 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | .. warning:: 2 | 3 | Pytest-Ethereum is still under active development, and yet to reach a stable release. It should not be used in production yet. 4 | 5 | Overview 6 | ======== 7 | 8 | This library is designed to make deploying and testing smart contracts simple, using `Py-EthPM` and `pytest`. 9 | 10 | 11 | Deployer 12 | -------- 13 | 14 | This library exposes a ``Deployer`` fixture to help create contract instances for any contract types available in the manifest that generated the ``Deployer`` instance. To create a ``Deployer`` instance, you must provide a ``pathlib.Path`` object pointing towards a valid manifest according to the `EthPM Specification `__. 15 | 16 | To deploy any of the available `contract types` onto the default ``w3`` instance, simply call ``deploy`` on the deployer and a newly created ``Package`` instance (which contains the newly created contract instance in its `deployments`) will be returned, along with the address of the newly deployed contract type. 17 | 18 | .. code:: python 19 | 20 | from pathlib import Path 21 | from ethpm import Package 22 | from eth_utils import is_address 23 | 24 | @pytest.fixture 25 | def owned_deployer(deployer): 26 | owned_manifest_path = Path('to/owned/manifest.json') 27 | return deployer(owned_manifest_path) 28 | 29 | def test_owned_contract(owned_deployer) 30 | owned_package = owned_deployer.deploy("owned") 31 | assert isinstance(owned_package, Package) 32 | owned_contract_instance = owned_package.deployments.get_deployment_instance("Owned") 33 | assert is_address(owned_contract_instance.address) 34 | 35 | 36 | .. py:method:: Deployer.deploy(contract_type) 37 | 38 | Returns a ``Package`` instance, containing a freshly deployed instance of the given `contract_type` (if sufficient data is present in the manifest). To add transaction kwargs (i.e. "from"), pass them in as a dict to the ``transaction`` keyword. 39 | 40 | .. code:: python 41 | 42 | deploy("Contract", arg1, transaction={"from": web3.eth.accounts[1]}) 43 | 44 | .. py:method:: Deployer.register_strategy(contract_type, strategy) 45 | 46 | If a `contract_type` requires linking, then you *must* register a valid strategy constructed with the ``Linker`` before you can deploy an instance of the `contract_type`. 47 | 48 | 49 | Linker 50 | ------ 51 | 52 | If a contract factory requires linking, you must register a "strategy" for a particular contract factory with the deployer. It is up to you to design an appropriate strategy for a contract factory. 53 | 54 | Three ``linker`` functions are made available: 55 | 56 | .. py:method:: deploy(contract_name, *args=None) 57 | 58 | To deploy an instance of `contract_name`. If the contract constructor requires arguments, they must also be passed in. 59 | 60 | .. py:method:: link(contract_name, linked_type) 61 | 62 | Links a `contract_name` to a `linked_type`. The `linked_type` must have already been deployed. 63 | 64 | .. py:method:: run_python(callback_fn) 65 | 66 | Calls any user-defined `callback_fn` on the contracts available in the active `Package`. This can be used to call specific functions on a contract if they are part of the setup. Returns the original, unmodified `Package` that was passed in. 67 | 68 | 69 | For example, the `Escrow` contract factory requires linking to an instance of the `SafeSendLib` before an `Escrow` contract instance can be deployed. This is how you would set up a strategy for `Escrow` 70 | 71 | .. code:: python 72 | 73 | @pytest.fixture 74 | def escrow_deployer(deployer, manifest_dir): 75 | escrow_manifest_path = manifest_dir / "escrow_manifest.json" 76 | return deployer(escrow_manifest_path) 77 | 78 | 79 | @pytest.fixture 80 | def escrow_contract_instance(escrow_deployer, w3): 81 | escrow_strategy = linker( 82 | deploy("SafeSendLib"), 83 | link("Escrow", "SafeSendLib"), 84 | deploy("Escrow", w3.eth.accounts[0]), 85 | ) 86 | escrow_deployer.register_strategy("Escrow", escrow_strategy) 87 | linked_escrow_package, _ = escrow_deployer.deploy("Escrow") 88 | return linked_escrow_package.deployments.get_deployment("Escrow") 89 | 90 | 91 | Log 92 | --- 93 | 94 | The ``Log`` class is available to help with testing for contract events, and the contents of the emitted logs. 95 | 96 | 97 | ``tests/fixtures/ping.vy`` 98 | 99 | .. include:: ../tests/manifests/ping/contracts/ping.vy 100 | :code: python 101 | 102 | 103 | .. code:: python 104 | 105 | # SETUP 106 | ping_package = deployer.deploy("ping") 107 | ping_instance = ping_package.deployments.get_contract_instance("ping") 108 | tx_hash = ping_instance.functions.ping(b"one", b"two") 109 | receipt = w3.eth.waitForTransactionReceipt(tx_hash) 110 | 111 | 112 | .. autoclass:: pytest_ethereum.testing.Log 113 | :members: is_present, not_present, exact_match 114 | -------------------------------------------------------------------------------- /docs/releases.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | v0.1.0-alpha.1 5 | -------------- 6 | 7 | - Launched repository, claimed names for pip, RTD, github, etc 8 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts= -v --showlocals --durations 10 3 | python_paths= . 4 | xfail_strict=true 5 | 6 | [pytest-watch] 7 | runner= pytest --failed-first --maxfail=1 8 | -------------------------------------------------------------------------------- /pytest_ethereum/__init__.py: -------------------------------------------------------------------------------- 1 | from .testing import tx_fail # noqa: F401 2 | -------------------------------------------------------------------------------- /pytest_ethereum/_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/pytest-ethereum/5e45ded06469d5ca4d3b6b1d2b78ac6c7e7f9174/pytest_ethereum/_utils/__init__.py -------------------------------------------------------------------------------- /pytest_ethereum/_utils/abi.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import Any, Dict, Tuple 3 | 4 | 5 | def merge_args_and_kwargs( 6 | event_abi: Dict[str, Any], args: Any, kwargs: Any 7 | ) -> Tuple[Any, ...]: 8 | """ 9 | Borrowed / modified from 10 | https://github.com/ethereum/web3.py/blob/master/web3/_utils/abi.py 11 | """ 12 | if len(args) + len(kwargs) > len(event_abi.get("inputs", [])): 13 | raise TypeError( 14 | "Incorrect argument count. Expected <= '{0}'. Got '{1}'".format( 15 | len(event_abi["inputs"]), len(args) + len(kwargs) 16 | ) 17 | ) 18 | 19 | if not kwargs and not args: 20 | raise TypeError("No kwargs or args provided.") 21 | 22 | if not kwargs: 23 | return args 24 | 25 | args_as_kwargs = { 26 | arg_abi["name"]: arg for arg_abi, arg in zip(event_abi["inputs"], args) 27 | } 28 | duplicate_keys = set(args_as_kwargs).intersection(kwargs.keys()) 29 | if duplicate_keys: 30 | raise TypeError( 31 | "{fn_name}() got multiple values for argument(s) '{dups}'".format( 32 | fn_name=event_abi["name"], dups=", ".join(duplicate_keys) 33 | ) 34 | ) 35 | 36 | sorted_arg_names = [arg_abi["name"] for arg_abi in event_abi["inputs"]] 37 | 38 | unknown_kwargs = {key for key in kwargs.keys() if key not in sorted_arg_names} 39 | if unknown_kwargs: 40 | if event_abi.get("name"): 41 | raise TypeError( 42 | "{fn_name}() got unexpected keyword argument(s) '{dups}'".format( 43 | fn_name=event_abi.get("name"), dups=", ".join(unknown_kwargs) 44 | ) 45 | ) 46 | # show type instead of name in the error message incase key 'name' is missing. 47 | raise TypeError( 48 | "Type: '{_type}' got unexpected keyword argument(s) '{dups}'".format( 49 | _type=event_abi.get("type"), dups=", ".join(unknown_kwargs) 50 | ) 51 | ) 52 | 53 | sorted_args = list( 54 | zip( 55 | *sorted( 56 | itertools.chain(kwargs.items(), args_as_kwargs.items()), 57 | key=lambda kv: sorted_arg_names.index(kv[0]), 58 | ) 59 | ) 60 | ) 61 | if sorted_args: 62 | return sorted_args[1] 63 | else: 64 | return tuple() 65 | -------------------------------------------------------------------------------- /pytest_ethereum/_utils/linker.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Iterable, List, Tuple 2 | 3 | from eth_typing import URI, Address, Manifest 4 | from eth_utils import to_canonical_address, to_dict, to_hex, to_list 5 | from eth_utils.toolz import assoc, assoc_in, dissoc 6 | from ethpm import Package 7 | from ethpm.utils.chains import ( 8 | check_if_chain_matches_chain_uri, 9 | create_block_uri, 10 | get_genesis_block_hash, 11 | ) 12 | from web3 import Web3 13 | 14 | from pytest_ethereum.exceptions import LinkerError 15 | from pytest_ethereum.typing import TxReceipt 16 | 17 | 18 | def pluck_matching_uri(deployment_data: Dict[URI, Dict[str, str]], w3: Web3) -> URI: 19 | """ 20 | Return any blockchain uri that matches w3-connected chain, if one 21 | is present in the deployment data keys. 22 | """ 23 | for uri in deployment_data.keys(): 24 | if check_if_chain_matches_chain_uri(w3, uri): 25 | return uri 26 | raise LinkerError( 27 | f"No matching blockchain URI found in deployment_data: {list(deployment_data.keys())}, " 28 | "for w3 instance: {w3.__repr__()}." 29 | ) 30 | 31 | 32 | def contains_matching_uri(deployment_data: Dict[str, Dict[str, str]], w3: Web3) -> bool: 33 | """ 34 | Returns true if any blockchain uri in deployment data matches 35 | w3-connected chain. 36 | """ 37 | for uri in deployment_data.keys(): 38 | if check_if_chain_matches_chain_uri(w3, uri): 39 | return True 40 | return False 41 | 42 | 43 | def create_latest_block_uri(w3: Web3, tx_receipt: TxReceipt) -> URI: 44 | """ 45 | Creates a new block uri from data in w3 and provided tx_receipt. 46 | """ 47 | chain_id = to_hex(get_genesis_block_hash(w3)) 48 | block_hash = to_hex(tx_receipt.blockHash) 49 | return create_block_uri(chain_id, block_hash) 50 | 51 | 52 | def insert_deployment( 53 | package: Package, 54 | deployment_name: str, 55 | deployment_data: Dict[str, str], 56 | latest_block_uri: URI, 57 | ) -> Manifest: 58 | """ 59 | Returns a new manifest. If a matching chain uri is found in the old manifest, it will 60 | update the chain uri along with the new deployment data. If no match, it will simply add 61 | the new chain uri and deployment data. 62 | """ 63 | old_deployments_data = package.manifest.get("deployments") 64 | if old_deployments_data and contains_matching_uri(old_deployments_data, package.w3): 65 | old_chain_uri = pluck_matching_uri(old_deployments_data, package.w3) 66 | old_deployments_chain_data = old_deployments_data[old_chain_uri] 67 | # Replace specific on-chain deployment (i.e. deployment_name) 68 | new_deployments_chain_data_init = dissoc( 69 | old_deployments_chain_data, deployment_name 70 | ) 71 | new_deployments_chain_data = { 72 | **new_deployments_chain_data_init, 73 | **{deployment_name: deployment_data}, 74 | } 75 | # Replace all on-chain deployments 76 | new_deployments_data_init = dissoc( 77 | old_deployments_data, "deployments", old_chain_uri 78 | ) 79 | new_deployments_data = { 80 | **new_deployments_data_init, 81 | **{latest_block_uri: new_deployments_chain_data}, 82 | } 83 | return assoc(package.manifest, "deployments", new_deployments_data) 84 | 85 | return assoc_in( 86 | package.manifest, 87 | ("deployments", latest_block_uri, deployment_name), 88 | deployment_data, 89 | ) 90 | 91 | 92 | @to_dict 93 | def create_deployment_data( 94 | contract_name: str, 95 | new_address: Address, 96 | tx_receipt: TxReceipt, 97 | link_refs: List[Dict[str, Any]] = None, 98 | ) -> Iterable[Tuple[str, Any]]: 99 | yield "contract_type", contract_name 100 | yield "address", to_hex(new_address) 101 | yield "transaction", to_hex(tx_receipt.transactionHash) 102 | yield "block", to_hex(tx_receipt.blockHash) 103 | if link_refs: 104 | yield "runtime_bytecode", {"link_dependencies": create_link_dep(link_refs)} 105 | 106 | 107 | @to_list 108 | def create_link_dep(link_refs: List[Dict[str, Any]]) -> Iterable[Dict[str, Any]]: 109 | for link_ref in link_refs: 110 | yield { 111 | "offsets": link_ref["offsets"], 112 | "type": "reference", 113 | "value": link_ref["name"], 114 | } 115 | 116 | 117 | def get_deployment_address(linked_type: str, package: Package) -> Address: 118 | """ 119 | Return the address of a linked_type found in a package's manifest deployments. 120 | """ 121 | try: 122 | deployment_address = to_canonical_address( 123 | package.deployments.get(linked_type)["address"] 124 | ) 125 | except KeyError: 126 | raise LinkerError( 127 | f"Package data does not contain a valid deployment of {linked_type} on the " 128 | "current w3-connected chain." 129 | ) 130 | return deployment_address 131 | -------------------------------------------------------------------------------- /pytest_ethereum/deployer.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Tuple # noqa: F401 2 | 3 | from eth_typing import Address 4 | from ethpm import Package 5 | 6 | from pytest_ethereum.exceptions import DeployerError 7 | from pytest_ethereum.linker import deploy, linker 8 | 9 | 10 | class Deployer: 11 | def __init__(self, package: Package) -> None: 12 | if not isinstance(package, Package): 13 | raise TypeError( 14 | f"Expected a Package object, instead received {type(package)}." 15 | ) 16 | self.package = package 17 | self.strategies = {} # type: Dict[str, Callable[[Package], Package]] 18 | 19 | def deploy( 20 | self, contract_type: str, *args: Any, **kwargs: Any 21 | ) -> Tuple[Package, Address]: 22 | factory = self.package.get_contract_factory(contract_type) 23 | if contract_type in self.strategies: 24 | strategy = self.strategies[contract_type] 25 | return strategy(self.package) 26 | if factory.needs_bytecode_linking: 27 | raise DeployerError( 28 | "Unable to deploy an unlinked factory. " 29 | "Please register a strategy for this contract type." 30 | ) 31 | strategy = linker(deploy(contract_type, *args, **kwargs)) 32 | return strategy(self.package) 33 | 34 | def register_strategy( 35 | self, contract_type: str, strategy: Callable[[Package], Package] 36 | ) -> None: 37 | self.strategies[contract_type] = strategy 38 | -------------------------------------------------------------------------------- /pytest_ethereum/exceptions.py: -------------------------------------------------------------------------------- 1 | class PytestEthereumError(Exception): 2 | """ 3 | Base class for all Pytest-Ethereum errors. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class DeployerError(PytestEthereumError): 10 | """ 11 | Raised when the Deployer is unable to deploy a contract type. 12 | """ 13 | 14 | pass 15 | 16 | 17 | class LinkerError(PytestEthereumError): 18 | """ 19 | Raised when the Linker is unable to link two contract types. 20 | """ 21 | 22 | pass 23 | 24 | 25 | class LogError(PytestEthereumError): 26 | """ 27 | Raised when the Log class is instantiated with invalid arguments. 28 | """ 29 | 30 | pass 31 | -------------------------------------------------------------------------------- /pytest_ethereum/linker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Callable, Dict, Tuple 3 | 4 | from eth_typing import Address 5 | from eth_utils import to_canonical_address, to_checksum_address, to_hex 6 | from eth_utils.toolz import assoc_in, curry, pipe 7 | from ethpm import Package 8 | 9 | from pytest_ethereum._utils.linker import ( 10 | create_deployment_data, 11 | create_latest_block_uri, 12 | get_deployment_address, 13 | insert_deployment, 14 | ) 15 | from pytest_ethereum.exceptions import LinkerError 16 | 17 | logger = logging.getLogger("pytest_ethereum.linker") 18 | 19 | 20 | def linker(*args: Callable[..., Any]) -> Callable[..., Any]: 21 | return _linker(args) 22 | 23 | 24 | @curry 25 | def _linker(operations: Callable[..., Any], package: Package) -> Callable[..., Package]: 26 | return pipe(package, *operations) 27 | 28 | 29 | def deploy( 30 | contract_name: str, *args: Any, transaction: Dict[str, Any] = None 31 | ) -> Callable[..., Tuple[Package, Address]]: 32 | """ 33 | Return a newly created package and contract address. 34 | Will deploy the given contract_name, if data exists in package. If 35 | a deployment is found on the current w3 instance, it will return that deployment 36 | rather than creating a new instance. 37 | """ 38 | return _deploy(contract_name, args, transaction) 39 | 40 | 41 | @curry 42 | def _deploy( 43 | contract_name: str, args: Any, transaction: Dict[str, Any], package: Package 44 | ) -> Tuple[Package, Address]: 45 | # Deploy new instance 46 | factory = package.get_contract_factory(contract_name) 47 | if not factory.linked_references and factory.unlinked_references: 48 | raise LinkerError( 49 | f"Contract factory: {contract_name} is missing runtime link references, which are " 50 | "necessary to populate manifest deployments that have a link reference. If using the " 51 | "builder tool, use `contract_type(..., runtime_bytecode=True)`." 52 | ) 53 | tx_hash = factory.constructor(*args).transact(transaction) 54 | tx_receipt = package.w3.eth.waitForTransactionReceipt(tx_hash) 55 | address = to_canonical_address(tx_receipt.contractAddress) 56 | # Create manifest copy with new deployment instance 57 | latest_block_uri = create_latest_block_uri(package.w3, tx_receipt) 58 | deployment_data = create_deployment_data( 59 | contract_name, address, tx_receipt, factory.linked_references 60 | ) 61 | manifest = insert_deployment( 62 | package, contract_name, deployment_data, latest_block_uri 63 | ) 64 | logger.info("%s deployed." % contract_name) 65 | return Package(manifest, package.w3) 66 | 67 | 68 | @curry 69 | def link(contract: str, linked_type: str, package: Package) -> Package: 70 | """ 71 | Return a new package, created with a new manifest after applying the linked type 72 | reference to the contract factory. 73 | """ 74 | deployment_address = get_deployment_address(linked_type, package) 75 | unlinked_factory = package.get_contract_factory(contract) 76 | if not unlinked_factory.needs_bytecode_linking: 77 | raise LinkerError( 78 | f"Contract factory: {unlinked_factory.__repr__()} does not need bytecode linking, " 79 | "so it is not a valid contract type for link()" 80 | ) 81 | linked_factory = unlinked_factory.link_bytecode({linked_type: deployment_address}) 82 | # todo replace runtime_bytecode in manifest 83 | manifest = assoc_in( 84 | package.manifest, 85 | ("contract_types", contract, "deployment_bytecode", "bytecode"), 86 | to_hex(linked_factory.bytecode), 87 | ) 88 | logger.info( 89 | "%s linked to %s at address %s." 90 | % (contract, linked_type, to_checksum_address(deployment_address)) 91 | ) 92 | return Package(manifest, package.w3) 93 | 94 | 95 | @curry 96 | def run_python(callback_fn: Callable[..., None], package: Package) -> Package: 97 | """ 98 | Return the unmodified package, after performing any user-defined callback function on 99 | the contracts in the package. 100 | """ 101 | callback_fn(package) 102 | logger.info("%s python function ran." % callback_fn.__name__) 103 | return package 104 | -------------------------------------------------------------------------------- /pytest_ethereum/plugins.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Callable 4 | 5 | from ethpm import Package 6 | import pytest 7 | from web3 import Web3 8 | 9 | from pytest_ethereum.deployer import Deployer 10 | 11 | 12 | @pytest.fixture 13 | def w3() -> Web3: 14 | w3 = Web3(Web3.EthereumTesterProvider()) 15 | return w3 16 | 17 | 18 | @pytest.fixture 19 | def deployer(w3: Web3) -> Callable[[Path], Deployer]: 20 | """ 21 | Returns a `Deployer` instance composed from a `Package` instance 22 | generated from the manifest located at the provided `path` folder. 23 | """ 24 | 25 | def _deployer(path: Path) -> Deployer: 26 | manifest = json.loads(path.read_text()) 27 | package = Package(manifest, w3) 28 | return Deployer(package) 29 | 30 | return _deployer 31 | -------------------------------------------------------------------------------- /pytest_ethereum/testing.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from eth_tester.exceptions import TransactionFailed 4 | import pytest 5 | from web3.contract import ContractEvent 6 | 7 | from pytest_ethereum._utils.abi import merge_args_and_kwargs 8 | 9 | TxReceipt = Dict[str, Any] 10 | 11 | 12 | class Log: 13 | def __init__( 14 | self, contract_event: ContractEvent, *args: Any, **kwargs: Any 15 | ) -> None: 16 | """ 17 | The ``Log`` class is available to help with testing for contract events, 18 | and the contents of the emitted logs. 19 | """ 20 | self.event = contract_event() 21 | self.args = merge_args_and_kwargs(self.event.abi, args, kwargs=kwargs) 22 | self.kwargs = kwargs 23 | 24 | def is_present(self, receipt: TxReceipt) -> bool: 25 | """ 26 | Asserts that *every* member of ``args`` / ``kwargs`` is present in the emitted 27 | log dictionary values. 28 | 29 | .. code:: python 30 | 31 | assert Log(ping.events.Ping, b"one").is_present(receipt) 32 | assert Log(ping.events.Ping, first=b"one").is_present(receipt) 33 | assert Log(ping.events.Ping, first=b"one", second=b"two").is_present(receipt) 34 | assert Log(ping.events.Ping, b"missing").is_present(receipt) is False 35 | assert Log(ping.events.Ping, second=b"one").is_present(receipt) is False 36 | assert Log(ping.events.Ping, b"one", b"missing").is_present(receipt) is False 37 | 38 | """ 39 | logs = self._process_receipt(receipt) 40 | missing_args = [arg for arg in self.args if arg not in logs.values()] 41 | if missing_args: 42 | return False 43 | return True 44 | 45 | def not_present(self, receipt: TxReceipt) -> bool: 46 | """ 47 | Asserts that *every* member of ``args`` / ``kwargs`` are *not* present in the 48 | emitted log dictionary values. 49 | 50 | .. code:: python 51 | 52 | assert Log(ping.events.Ping, b"missing").not_present(receipt) 53 | assert Log(ping.events.Ping, first=b"missing").not_present(receipt) 54 | assert Log(ping.events.Ping, b"one").not_present(receipt) is False 55 | assert Log(ping.events.Ping, first=b"one").not_present(receipt) is False 56 | assert Log(ping.events.Ping, b"one", b"missing").not_present(receipt) is False 57 | """ 58 | logs = self._process_receipt(receipt) 59 | matching_args = [arg for arg in self.args if arg in logs.values()] 60 | if matching_args: 61 | return False 62 | return True 63 | 64 | def exact_match(self, receipt: TxReceipt) -> bool: 65 | """ 66 | Asserts that the provided ``kwargs`` match *exactly* the emitted log dictionary. 67 | Requires ``**kwargs``, and does not accept ``*args``. 68 | 69 | .. code:: python 70 | 71 | assert Log(ping.events.Ping, first=b"one", second=b"two").exact_match(receipt) 72 | assert Log(ping.events.Ping, first=b"one").exact_match(receipt) is False 73 | assert Log(ping.events.Ping, first=b"not_present").exact_match(receipt) is False 74 | """ 75 | if not self.kwargs: 76 | raise TypeError( 77 | "Log().exact_match() requires keyword arguments to test an exact match." 78 | ) 79 | 80 | logs = self._process_receipt(receipt) 81 | if self.kwargs != logs: 82 | return False 83 | return True 84 | 85 | def _process_receipt(self, receipt: TxReceipt) -> Dict[str, bytes]: 86 | processed_receipt = self.event.processReceipt(receipt)[0] 87 | return {k: v for k, v in processed_receipt["args"].items()} 88 | 89 | 90 | def tx_fail(*args: Any, **kwargs: Any) -> None: 91 | return pytest.raises(TransactionFailed, *args, **kwargs) 92 | -------------------------------------------------------------------------------- /pytest_ethereum/typing.py: -------------------------------------------------------------------------------- 1 | # todo: move to eth-typing 2 | class TxReceipt: 3 | blockHash = None 4 | transactionHash = None 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import ( 4 | setup, 5 | find_packages, 6 | ) 7 | 8 | extras_require = { 9 | 'test': [ 10 | "pytest>=4.4.0", 11 | "pytest-xdist", 12 | "tox>=2.9.1,<3", 13 | ], 14 | 'lint': [ 15 | 'black>=18.6b4,<19', 16 | "flake8==3.4.1", 17 | "isort>=4.2.15,<5", 18 | "mypy<0.600", 19 | ], 20 | 'doc': [ 21 | "Sphinx>=1.6.5,<2", 22 | "sphinx_rtd_theme>=0.1.9", 23 | ], 24 | 'dev': [ 25 | "bumpversion>=0.5.3,<1", 26 | "pytest-watch>=4.1.0,<5", 27 | "wheel", 28 | "twine", 29 | "ipython", 30 | ], 31 | } 32 | 33 | extras_require['dev'] = ( 34 | extras_require['dev'] + 35 | extras_require['test'] + 36 | extras_require['lint'] + 37 | extras_require['doc'] 38 | ) 39 | 40 | setup( 41 | name='pytest-ethereum', 42 | # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme 43 | version='0.1.3-alpha.7', 44 | description="""pytest-ethereum: Pytest library for ethereum projects.""", 45 | long_description_markdown_filename='README.md', 46 | author='pytest-ethereum-contributors', 47 | author_email='', 48 | url='https://github.com/ethereum/pytest-ethereum', 49 | include_package_data=True, 50 | install_requires=[ 51 | "eth-utils>=1.4.0,<2.0.0", 52 | "ethpm>=0.1.4a14,<1.0.0", 53 | ], 54 | setup_requires=['setuptools-markdown'], 55 | python_requires='>=3.6, <4', 56 | extras_require=extras_require, 57 | entry_points={"pytest11": ["pytest_ethereum = pytest_ethereum.plugins"]}, 58 | py_modules=['pytest_ethereum'], 59 | license="MIT", 60 | zip_safe=False, 61 | keywords='ethereum', 62 | packages=find_packages(exclude=["tests", "tests.*"]), 63 | classifiers=[ 64 | 'Development Status :: 2 - Pre-Alpha', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: MIT License', 67 | 'Natural Language :: English', 68 | 'Programming Language :: Python :: 3', 69 | 'Programming Language :: Python :: 3.6', 70 | ], 71 | ) 72 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from ethpm import ASSETS_DIR 4 | import pytest 5 | 6 | TESTS_DIR = Path(__file__).parent 7 | 8 | 9 | @pytest.fixture 10 | def manifest_dir(): 11 | return TESTS_DIR / "manifests" 12 | 13 | 14 | # LINK REFS 15 | @pytest.fixture 16 | def escrow_deployer(deployer): 17 | escrow_manifest_path = ASSETS_DIR / "escrow" / "1.0.2.json" 18 | return deployer(escrow_manifest_path) 19 | -------------------------------------------------------------------------------- /tests/core/test_deployer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from eth_utils import is_address 4 | from ethpm import ASSETS_DIR 5 | import pytest 6 | import web3 7 | 8 | from pytest_ethereum.exceptions import DeployerError 9 | 10 | logging.getLogger("evm").setLevel(logging.INFO) 11 | 12 | 13 | # 14 | # Vyper Contracts 15 | # 16 | 17 | 18 | # User Code 19 | @pytest.fixture 20 | def greeter(deployer, manifest_dir): 21 | return deployer(manifest_dir / "greeter" / "1.0.0.json").deploy("greeter") 22 | 23 | 24 | @pytest.fixture 25 | def registry(deployer, manifest_dir): 26 | return deployer(manifest_dir / "registry" / "1.0.0.json").deploy("registry") 27 | 28 | 29 | def test_user_code_with_fixture(greeter, registry): 30 | greeter_instance = greeter.deployments.get_instance("greeter") 31 | # test registry 32 | assert isinstance(greeter_instance, web3.contract.Contract) 33 | registry_instance = registry.deployments.get_instance("registry") 34 | assert tuple(registry_instance.functions) == ("register", "lookup") 35 | # test greeter 36 | assert isinstance(registry_instance, web3.contract.Contract) 37 | greeting = greeter_instance.functions.greet().call() 38 | assert greeting == b"Hello" 39 | 40 | 41 | # 42 | # Solidity Compiler Output 43 | # 44 | 45 | 46 | # SIMPLE 47 | @pytest.fixture 48 | def owned(deployer): 49 | owned_manifest_path = ASSETS_DIR / "owned" / "1.0.1.json" 50 | owned_deployer = deployer(path=owned_manifest_path) 51 | return owned_deployer.deploy("Owned") 52 | 53 | 54 | def test_owned_deployer(owned): 55 | owned_contract_instance = owned.deployments.get_instance("Owned") 56 | assert is_address(owned_contract_instance.address) 57 | 58 | 59 | # CONSTRUCTOR ARGS 60 | @pytest.fixture 61 | def standard_token(deployer): 62 | standard_token_manifest_path = ASSETS_DIR / "standard-token" / "1.0.1.json" 63 | standard_token_deployer = deployer(standard_token_manifest_path) 64 | return standard_token_deployer.deploy("StandardToken", 100) 65 | 66 | 67 | def test_standard_token_deployer(standard_token): 68 | standard_token_instance = standard_token.deployments.get_instance("StandardToken") 69 | assert standard_token_instance.functions.totalSupply().call() == 100 70 | 71 | 72 | # LIBRARY 73 | @pytest.fixture 74 | def safe_math(deployer): 75 | safe_math_manifest_path = ASSETS_DIR / "safe-math-lib" / "1.0.1.json" 76 | safe_math_deployer = deployer(safe_math_manifest_path) 77 | return safe_math_deployer.deploy("SafeMathLib") 78 | 79 | 80 | def test_safe_math_deployer(safe_math): 81 | safe_math_instance = safe_math.deployments.get_instance("SafeMathLib") 82 | assert is_address(safe_math_instance.address) 83 | 84 | 85 | def test_escrow_deployer_unlinked(escrow_deployer): 86 | with pytest.raises(DeployerError): 87 | escrow_deployer.deploy("Escrow", escrow_deployer.package.w3.eth.accounts[0]) 88 | -------------------------------------------------------------------------------- /tests/core/test_import.py: -------------------------------------------------------------------------------- 1 | def test_import(): 2 | import pytest_ethereum # noqa: F401 3 | -------------------------------------------------------------------------------- /tests/core/test_linker.py: -------------------------------------------------------------------------------- 1 | from ethpm import ASSETS_DIR, Package 2 | import pytest 3 | 4 | from pytest_ethereum.deployer import Deployer 5 | from pytest_ethereum.exceptions import DeployerError 6 | from pytest_ethereum.linker import deploy, link, linker, run_python 7 | 8 | 9 | @pytest.fixture 10 | def escrow_deployer(deployer): 11 | escrow_manifest_path = ASSETS_DIR / "escrow" / "1.0.3.json" 12 | return deployer(escrow_manifest_path) 13 | 14 | 15 | def test_linker(escrow_deployer, w3): 16 | # todo test multiple links in one type 17 | assert isinstance(escrow_deployer, Deployer) 18 | with pytest.raises(DeployerError): 19 | escrow_deployer.deploy("Escrow") 20 | 21 | escrow_strategy = linker( 22 | deploy("SafeSendLib"), 23 | link("Escrow", "SafeSendLib"), 24 | deploy("Escrow", w3.eth.accounts[0]), 25 | ) 26 | assert hasattr(escrow_strategy, "__call__") 27 | escrow_deployer.register_strategy("Escrow", escrow_strategy) 28 | linked_escrow_package = escrow_deployer.deploy("Escrow") 29 | assert isinstance(linked_escrow_package, Package) 30 | linked_escrow_factory = linked_escrow_package.get_contract_factory("Escrow") 31 | assert linked_escrow_factory.needs_bytecode_linking is False 32 | 33 | 34 | def test_linker_with_from(escrow_deployer, w3): 35 | escrow_strategy = linker( 36 | deploy("SafeSendLib"), 37 | link("Escrow", "SafeSendLib"), 38 | deploy("Escrow", w3.eth.accounts[0], transaction={"from": w3.eth.accounts[5]}), 39 | ) 40 | escrow_deployer.register_strategy("Escrow", escrow_strategy) 41 | linked_escrow_package = escrow_deployer.deploy("Escrow") 42 | escrow_instance = linked_escrow_package.deployments.get_instance("Escrow") 43 | assert escrow_instance.functions.sender().call() == w3.eth.accounts[5] 44 | 45 | 46 | def test_linker_with_callback(escrow_deployer, w3): 47 | sender = w3.eth.accounts[0] 48 | recipient = w3.eth.accounts[5] 49 | 50 | def callback_fn(package): 51 | escrow_instance = package.deployments.get_instance("Escrow") 52 | tx_hash = escrow_instance.functions.releaseFunds().transact({"from": sender}) 53 | w3.eth.waitForTransactionReceipt(tx_hash) 54 | 55 | escrow_strategy = linker( 56 | deploy("SafeSendLib", transaction={"from": sender}), 57 | link("Escrow", "SafeSendLib"), 58 | deploy( 59 | "Escrow", 60 | recipient, 61 | transaction={"from": sender, "value": w3.toWei("1", "ether")}, 62 | ), 63 | run_python(callback_fn), 64 | ) 65 | escrow_deployer.register_strategy("Escrow", escrow_strategy) 66 | assert w3.eth.getBalance(recipient) == w3.toWei("1000000", "ether") 67 | linked_escrow_package = escrow_deployer.deploy("Escrow") 68 | escrow_instance = linked_escrow_package.deployments.get_instance("Escrow") 69 | assert escrow_instance.functions.sender().call() == sender 70 | assert w3.eth.getBalance(recipient) == w3.toWei("1000001", "ether") 71 | -------------------------------------------------------------------------------- /tests/core/test_log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from pytest_ethereum.testing import Log 6 | 7 | logging.getLogger("evm").setLevel(logging.INFO) 8 | 9 | 10 | @pytest.fixture 11 | def ping_setup(deployer, manifest_dir): 12 | ping_deployer = deployer(manifest_dir / "ping" / "1.0.0.json") 13 | ping_package = ping_deployer.deploy("ping") 14 | ping = ping_package.deployments.get_instance("ping") 15 | tx_hash = ping.functions.ping(b"1", b"2").transact() 16 | receipt = ping_package.w3.eth.waitForTransactionReceipt(tx_hash) 17 | return ping, receipt 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "args,kwargs,expected", 22 | ( 23 | ((b"1".ljust(32, b"\00"),), {}, True), 24 | ((), {"first": b"1".ljust(32, b"\00")}, True), 25 | ((), {"first": b"1".ljust(32, b"\00"), "second": b"2".ljust(32, b"\00")}, True), 26 | ((b"1".ljust(32, b"\00"), b"2".ljust(32, b"\00")), {}, True), 27 | ((b"1".ljust(32, b"\00"),), {"second": b"2".ljust(32, b"\00")}, True), 28 | ((b"3".ljust(32, b"\00"),), {}, False), 29 | ((), {"first": b"3".ljust(32, b"\00")}, False), 30 | ((b"1".ljust(32, b"\00"),), {"second": b"3".ljust(32, b"\00")}, False), 31 | ), 32 | ) 33 | def test_log_is_present(ping_setup, w3, args, kwargs, expected): 34 | ping, receipt = ping_setup 35 | # Asserts that every arg is present in event log values 36 | assert Log(ping.events.Ping, *args, **kwargs).is_present(receipt) is expected 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "args,kwargs", 41 | ( 42 | ((), {}), 43 | ((), {"invalid": b"1"}), 44 | ((), {"first": b"1", "invalid": b"2"}), 45 | ((b"1"), {"first": b"2"}), 46 | ), 47 | ) 48 | def test_log_is_present_raises_exception_with_invalid_args_kwargs( 49 | ping_setup, args, kwargs 50 | ): 51 | ping, receipt = ping_setup 52 | with pytest.raises(TypeError): 53 | Log(ping.events.Ping, *args, **kwargs).is_present(receipt) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "args,kwargs,expected", 58 | ( 59 | ((), {"first": b"1".ljust(32, b"\00"), "second": b"2".ljust(32, b"\00")}, True), 60 | ((), {"first": b"1".ljust(32, b"\00")}, False), 61 | ((), {"second": b"2".ljust(32, b"\00")}, False), 62 | ), 63 | ) 64 | def test_log_exact_match(ping_setup, w3, args, kwargs, expected): 65 | ping, receipt = ping_setup 66 | # Requires *kwargs, asserts that kwargs match exactly event logs 67 | assert Log(ping.events.Ping, *args, **kwargs).exact_match(receipt) is expected 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "args,kwargs", (((), {}), ((b"1"), {}), ((b"1"), {"first": b"2"})) 72 | ) 73 | def test_log_exact_match_raises_exception_with_invalid_args_kwargs( 74 | ping_setup, args, kwargs 75 | ): 76 | ping, receipt = ping_setup 77 | with pytest.raises(TypeError): 78 | Log(ping.events.Ping, *args, **kwargs).exact_match(receipt) 79 | 80 | 81 | @pytest.mark.parametrize( 82 | "kwargs", (({"invalid": b"1"}), ({"first": b"1", "invalid": b"2"})) 83 | ) 84 | def test_invalid_keywords_raise_exception_on_log_instantiation(ping_setup, kwargs): 85 | ping, receipt = ping_setup 86 | with pytest.raises(TypeError): 87 | Log(ping.events.Ping, **kwargs) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "args,kwargs,expected", 92 | ( 93 | ((b"y".ljust(32, b"\00"),), {}, True), 94 | ((), {"first": b"y".ljust(32, b"\00")}, True), 95 | ((), {"first": b"1".ljust(32, b"\00")}, False), 96 | ((b"y",), {"second": b"1".ljust(32, b"\00")}, False), 97 | ((b"1",), {"second": b"2".ljust(32, b"\00")}, False), 98 | ((b"y",), {"second": b"1".ljust(32, b"\00")}, False), 99 | ( 100 | (), 101 | {"first": b"y".ljust(32, b"\00"), "second": b"1".ljust(32, b"\00")}, 102 | False, 103 | ), 104 | ), 105 | ) 106 | def test_log_not_present(ping_setup, w3, args, kwargs, expected): 107 | ping, receipt = ping_setup 108 | # asserts that every args is not in event log values 109 | assert Log(ping.events.Ping, *args, **kwargs).not_present(receipt) is expected 110 | 111 | 112 | @pytest.mark.parametrize("args,kwargs", (((), {}), ((b"1"), {"first": b"2"}))) 113 | def test_log_not_present_raises_exception_with_invalid_args_kwargs( 114 | ping_setup, args, kwargs 115 | ): 116 | ping, receipt = ping_setup 117 | with pytest.raises(TypeError): 118 | Log(ping.events.Ping, *args, **kwargs).not_present(receipt) 119 | -------------------------------------------------------------------------------- /tests/core/test_w3_fixture.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | 4 | def test_w3_fixture_is_available(request): 5 | w3 = request.getfixturevalue("w3") 6 | assert isinstance(w3, Web3) 7 | -------------------------------------------------------------------------------- /tests/core/utils/test_linker_utils.py: -------------------------------------------------------------------------------- 1 | from eth_utils import remove_0x_prefix, to_hex 2 | from eth_utils.toolz import assoc 3 | from ethpm.utils.chains import create_block_uri, get_genesis_block_hash 4 | import pytest 5 | 6 | from pytest_ethereum._utils.linker import ( 7 | contains_matching_uri, 8 | insert_deployment, 9 | pluck_matching_uri, 10 | ) 11 | from pytest_ethereum.exceptions import LinkerError 12 | 13 | 14 | @pytest.fixture 15 | def chain_setup(w3): 16 | old_chain_id = remove_0x_prefix(to_hex(get_genesis_block_hash(w3))) 17 | block_hash = remove_0x_prefix(to_hex(w3.eth.getBlock("earliest").hash)) 18 | old_chain_uri = f"blockchain://{old_chain_id}/block/{block_hash}" 19 | match_data = { 20 | old_chain_uri: {"x": "x"}, 21 | f"blockchain://1234/block/{block_hash}": {"x": "x"}, 22 | } 23 | no_match_data = { 24 | f"blockchain://56775ac59d0774e6b603a79c4218efeb5653b99ba0ff14db983bac2662251a8a/block/{block_hash}": { # noqa: E501 25 | "x": "x" 26 | } 27 | } 28 | return w3, match_data, no_match_data, old_chain_uri 29 | 30 | 31 | def test_pluck_matching_uri(chain_setup): 32 | w3, match_data, no_match_data, old_chain_uri = chain_setup 33 | 34 | assert pluck_matching_uri(match_data, w3) == old_chain_uri 35 | with pytest.raises(LinkerError): 36 | assert pluck_matching_uri(no_match_data, w3) 37 | 38 | 39 | def test_contains_matching_uri(chain_setup): 40 | w3, match_data, no_match_data, _ = chain_setup 41 | 42 | assert contains_matching_uri(match_data, w3) is True 43 | assert contains_matching_uri(no_match_data, w3) is False 44 | 45 | 46 | def test_insert_deployment(escrow_deployer): 47 | w3 = escrow_deployer.package.w3 48 | escrow_package = escrow_deployer.package 49 | init_deployment_data = { 50 | "contract_type": "Escrow", 51 | "address": "0x", 52 | "transaction": "0x", 53 | "block": "0x", 54 | } 55 | new_deployment_data = { 56 | "contract_type": "Escrow", 57 | "address": "0x123", 58 | "transaction": "0x123", 59 | "block": "0x123", 60 | } 61 | genesis_hash = to_hex(get_genesis_block_hash(w3)) 62 | w3.testing.mine(1) 63 | init_block_hash = to_hex(w3.eth.getBlock("latest")["hash"]) 64 | init_block_uri = create_block_uri(genesis_hash, init_block_hash) 65 | alt_block_uri = init_block_uri[:15] + "yxz123" + init_block_uri[21:] 66 | init_block_deployment_data = { 67 | init_block_uri: {"Other": {"x": "x"}, "Escrow": init_deployment_data}, 68 | alt_block_uri: {"alt": {"x": "x"}}, 69 | } 70 | w3.testing.mine(1) 71 | new_block_hash = to_hex(w3.eth.getBlock("latest")["hash"]) 72 | new_block_uri = create_block_uri(genesis_hash, new_block_hash) 73 | escrow_package.manifest = assoc( 74 | escrow_package.manifest, "deployments", init_block_deployment_data 75 | ) 76 | updated_manifest = insert_deployment( 77 | escrow_package, "Escrow", new_deployment_data, new_block_uri 78 | ) 79 | expected_deployments_data = { 80 | new_block_uri: {"Other": {"x": "x"}, "Escrow": new_deployment_data}, 81 | alt_block_uri: {"alt": {"x": "x"}}, 82 | } 83 | assert updated_manifest["deployments"] == expected_deployments_data 84 | -------------------------------------------------------------------------------- /tests/manifests/greeter/1.0.0.json: -------------------------------------------------------------------------------- 1 | {"contract_types":{"greeter":{"abi":[{"constant":false,"inputs":[],"name":"__init__","outputs":[],"payable":false,"type":"constructor"},{"constant":false,"gas":70954,"inputs":[{"name":"x","type":"bytes"}],"name":"setGreeting","outputs":[],"payable":false,"type":"function"},{"constant":false,"gas":4486,"inputs":[],"name":"greet","outputs":[{"name":"out","type":"bytes"}],"payable":false,"type":"function"}],"deployment_bytecode":{"bytecode":"0x600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052341561009e57600080fd5b6005610140527f48656c6c6f0000000000000000000000000000000000000000000000000000006101605261014080600060c052602060c020602082510161012060006002818352015b826101205160200211156100fb5761011d565b61012051602002850151610120518501555b81516001018083528114156100e8575b50505050505061032f56600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a05263b8e46d3a600051141561013057602060046101403734156100b457600080fd5b60346004356004016101603760146004356004013511156100d457600080fd5b61016080600060c052602060c020602082510161012060006002818352015b8261012051602002111561010657610128565b61012051602002850151610120518501555b81516001018083528114156100f3575b505050505050005b63cfae3217600051141561020157341561014957600080fd5b60008060c052602060c020610180602082540161012060006002818352015b8261012051602002111561017b5761019d565b61012051850154610120516020028501525b8151600101808352811415610168575b5050505050506101e0610180516014818352015b60146101e05111156101c2576101de565b60006101e0516101a001535b81516001018083528114156101b1575b50506020610160526040610180510160206001820306601f8201039050610160f3005b60006000fd5b61012861032f0361012860003961012861032f036000f3"}}},"deployments":{"blockchain://0817570684a8b349c2e15aa1a5ba48d269335d487bcf0d7a2f3ef1f9f764d5e4/block/03d7c71f9f8d8c29a17c00a0d5ce262ad243a575ce5023c430e58dbb02342901":{"greeter":{"address":"0xf2e246bb76df876cef8b38ae84130f4f55de395b","block":"0x03d7c71f9f8d8c29a17c00a0d5ce262ad243a575ce5023c430e58dbb02342901","contract_type":"greeter","transaction":"0x56e0b0d02c8f11e62937101ec419899b4f4a8be8ceec825eb9afb797f79e7262"}}},"manifest_version":"2","package_name":"greeter","sources":{"./contracts/greeter.vy":"# Vyper Greeter Contract\n\ngreeting: bytes[20]\n\n\n@public\ndef __init__():\n self.greeting = \"Hello\"\n\n\n@public\ndef setGreeting(x: bytes[20]):\n self.greeting = x\n\n\n@public\ndef greet() -> bytes[40]:\n return self.greeting\n"},"version":"1.0.0"} 2 | -------------------------------------------------------------------------------- /tests/manifests/ping/1.0.0.json: -------------------------------------------------------------------------------- 1 | {"contract_types":{"ping":{"abi":[{"anonymous":false,"inputs":[{"indexed":true,"name":"first","type":"bytes32"},{"indexed":false,"name":"second","type":"bytes32"}],"name":"Ping","type":"event"},{"constant":false,"inputs":[],"name":"__init__","outputs":[],"payable":false,"type":"constructor"},{"constant":false,"gas":2023,"inputs":[{"name":"_first","type":"bytes32"},{"name":"_second","type":"bytes32"}],"name":"ping","outputs":[],"payable":false,"type":"function"}],"deployment_bytecode":{"bytecode":"0x600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052341561009e57600080fd5b61019256600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a05263ecf265ee60005114156100e957604060046101403734156100b457600080fd5b6101605161018052610140517fd3dd553ac7b6f7eeed331fe5d44e31765c5bd75bb823d01f68fc115c1aaf17c56020610180a2005b60006000fd5b6100a3610192036100a36000396100a3610192036000f3"}}},"deployments":{"blockchain://ecc9320286c30c7526030384a99239cbc50dd0fc0df2ec6a687ccc3aa1f07d33/block/c346070329cf54495d44fc115e4b1a73a7dacdd531d6d0b94cc7d56715209039":{"ping":{"address":"0xf2e246bb76df876cef8b38ae84130f4f55de395b","block":"0xc346070329cf54495d44fc115e4b1a73a7dacdd531d6d0b94cc7d56715209039","contract_type":"ping","transaction":"0x061b45b64334284220214682827c07d80db6855d2014fd378d51b6d47c471a37"}}},"manifest_version":"2","package_name":"ping","sources":{"./contracts/ping.vy":"Ping: event({first: indexed(bytes32), second: bytes32})\n\n@public\ndef __init__():\n pass\n\n@public\ndef ping(_first: bytes32, _second: bytes32):\n log.Ping(_first, _second)\n"},"version":"1.0.0"} 2 | -------------------------------------------------------------------------------- /tests/manifests/ping/contracts/ping.vy: -------------------------------------------------------------------------------- 1 | Ping: event({first: indexed(bytes32), second: bytes32}) 2 | 3 | @public 4 | def __init__(): 5 | pass 6 | 7 | @public 8 | def ping(_first: bytes32, _second: bytes32): 9 | log.Ping(_first, _second) 10 | -------------------------------------------------------------------------------- /tests/manifests/registry/1.0.0.json: -------------------------------------------------------------------------------- 1 | {"contract_types":{"registry":{"abi":[{"constant":false,"gas":36094,"inputs":[{"name":"name","type":"bytes"},{"name":"owner","type":"address"}],"name":"register","outputs":[],"payable":false,"type":"function"},{"constant":true,"gas":846,"inputs":[{"name":"name","type":"bytes"}],"name":"lookup","outputs":[{"name":"out","type":"address"}],"payable":false,"type":"function"}],"deployment_bytecode":{"bytecode":"0x61018d56600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a05263a99e7e29600051141561012257604060046101403734156100b457600080fd5b60846004356004016101803760646004356004013511156100d457600080fd5b60243560205181106100e557600080fd5b506000610180516101a02060e05260c052604060c020541561010657600080fd5b610160516000610180516101a02060e05260c052604060c02055005b63c3b2556d6000511415610183576020600461014037341561014357600080fd5b608460043560040161016037606460043560040135111561016357600080fd5b6000610160516101802060e05260c052604060c0205460005260206000f3005b60006000fd5b61000461018d0361000460003961000461018d036000f3"}}},"deployments":{"blockchain://f05efd1cf8fb7b754a5cecd95d98d10a8c1f8c9d1354959105c7f97745d6c37c/block/8d2e9ea9c7a8fcf552c72aa334fc624818f686c14d6e8cf2253acc4c7f87a323":{"registry":{"address":"0xf2e246bb76df876cef8b38ae84130f4f55de395b","block":"0x8d2e9ea9c7a8fcf552c72aa334fc624818f686c14d6e8cf2253acc4c7f87a323","contract_type":"registry","transaction":"0xd980e480e397362c91929f1aa175977030ffc70671d9b96778eb712d01268e98"}}},"manifest_version":"2","package_name":"registry","sources":{"./contracts/registry.vy":"registry: address[bytes[100]]\n\n\n@public\ndef register(name: bytes[100], owner: address):\n assert self.registry[name] == ZERO_ADDRESS # check name has not been set yet.\n self.registry[name] = owner\n\n\n@public\n@constant\ndef lookup(name: bytes[100]) -> address:\n return self.registry[name]\n"},"version":"1.0.0"} 2 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{36}-core 4 | lint 5 | doctest 6 | 7 | [isort] 8 | combine_as_imports=True 9 | force_sort_within_sections=True 10 | include_trailing_comma=True 11 | known_third_party=hypothesis,pytest 12 | known_first_party=pytest_ethereum 13 | line_length=88 14 | multi_line_output=3 15 | force_grid_wrap=0 16 | use_parentheses=True 17 | 18 | [flake8] 19 | max-line-length= 100 20 | exclude= venv*,.tox,docs,build 21 | ignore= 22 | 23 | [testenv] 24 | usedevelop=True 25 | commands= 26 | core: pytest {posargs:tests/core} 27 | doctest: make -C {toxinidir}/docs doctest 28 | basepython = 29 | doctest: python 30 | py36: python3.6 31 | extras= 32 | test 33 | doctest: doc 34 | whitelist_externals=make 35 | 36 | [testenv:lint] 37 | basepython=python 38 | extras=lint 39 | commands= 40 | flake8 {toxinidir}/pytest_ethereum {toxinidir}/tests 41 | mypy --follow-imports=silent --ignore-missing-imports --check-untyped-defs --disallow-incomplete-defs --disallow-untyped-defs --disallow-any-generics -p pytest_ethereum 42 | black --check --diff {toxinidir}/pytest_ethereum/ --check --diff {toxinidir}/tests/ 43 | isort --recursive {toxinidir}/pytest_ethereum {toxinidir}/tests 44 | --------------------------------------------------------------------------------