diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..a02043a --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,49 @@ +--- +name: Black + +defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + +on: + pull_request: + branches: [main, dev] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Black + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + pip install . + pip install deepdiff numpy + + mkdir -p .github/linters + cp pyproject.toml .github/linters + + - name: Black + uses: github/super-linter/slim@v4.9.2 + if: always() + env: + # run linter on everything to catch preexisting problems + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Run only black + VALIDATE_PYTHON_BLACK: true + PYTHON_BLACK_CONFIG_FILE: pyproject.toml \ No newline at end of file diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 0000000..3f2fedb --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,49 @@ +--- +name: Mypy + +defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + +on: + pull_request: + branches: [master, dev] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Mypy + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + pip install . + pip install types-setuptools + + mkdir -p .github/linters + cp mypy.ini .github/linters + + - name: Mypy + uses: github/super-linter/slim@v4.9.2 + if: always() + env: + # run linter on everything to catch preexisting problems + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Run only black + VALIDATE_PYTHON_MYPY: true + PYTHON_MYPY_CONFIG_FILE: mypy.ini \ No newline at end of file diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..22aea31 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,50 @@ +--- +name: Pylint + +defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + +on: + pull_request: + branches: [main, dev] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Pylint + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + pip install . + pip install deepdiff numpy + + mkdir -p .github/linters + cp pyproject.toml .github/linters + + - name: Pylint + uses: github/super-linter/slim@v4.9.2 + if: always() + env: + # run linter on everything to catch preexisting problems + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Run only pylint + VALIDATE_PYTHON: true + VALIDATE_PYTHON_PYLINT: true + PYTHON_PYLINT_CONFIG_FILE: pyproject.toml \ No newline at end of file diff --git a/README.md b/README.md index 717a7c1..7a2c4be 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,19 @@ # Automated tool for generating Foundry unit tests for smart contract fuzzer failed properties -`test-generator` is a Python tool that generates unit tests from [Echidna](https://github.com/crytic/echidna) and [Medusa](https://github.com/crytic/medusa/tree/master) failed properties, using the generated reproducer files. It uses [Slither](https://github.com/crytic/slither) for determining types and jinja2 for generating strings from string templates. +`test-generator` is a Python tool that generates unit tests from [Echidna](https://github.com/crytic/echidna) and [Medusa](https://github.com/crytic/medusa/tree/master) failed properties, using the generated reproducer files. It uses [Slither](https://github.com/crytic/slither) for determining types and jinja2 for generating the test files using string templates. + +**Disclaimer**: Please note that `test-generator` is **under development**. Currently, not all Solidity types are supported and some types (like `bytes*`, and `string`) might be improperly decoded from the corpora call sequences. + +## Features +`test-generator` provides support for: +- ✔️ Generating Foundry unit tests from the fuzzer corpus of single entry point fuzzing harnesses +- ✔️ Medusa and Echidna corpora +- ✔️ Solidity types: `bool`,`uint*`, `int*`, `address`, `struct`, `enum`, single-dimensional fixed-size arrays and dynamic arrays, multi-dimensional fixed-size arrays. ## Installation and pre-requisites -In order to be able to use test-generator, you will need to install it first: +In order to be able to use `test-generator`, you will need to install it first: ```bash git clone git@github.com:crytic/test-generator.git @@ -14,20 +22,16 @@ cd test-generator pip3 install -e . ``` -These commands will install all the Python libraries and tools required to run test-generator. However, it won't install Echidna or Medusa, so you will need to download and install the latest version yourself from its official releases ([Echidna](https://github.com/crytic/echidna/releases), [Medusa](https://github.com/crytic/medusa/releases)). +These commands will install all the Python libraries and tools required to run `test-generator`. However, it won't install Echidna or Medusa, so you will need to download and install the latest version yourself from its official releases ([Echidna](https://github.com/crytic/echidna/releases), [Medusa](https://github.com/crytic/medusa/releases)). ## Example -In order to generate a test file for the [BasicTypes.sol](test/src/BasicTypes.sol) contract, based on the Echidna corpus reproducers for this contract ([corpus-basic](test/echidna-corpora/corpus-basic/)), we need to `cd` into the `test` directory which contains the Foundry project and run the command: -``` +In order to generate a test file for the [BasicTypes.sol](test/src/BasicTypes.sol) contract, based on the Echidna corpus reproducers for this contract ([corpus-basic](tests/test_data/echidna-corpora/corpus-basic/)), we need to `cd` into the `tests/test_data` directory which contains the Foundry project and run the command: +```bash test-generator ./src/BasicTypes.sol --corpus-dir echidna-corpora/corpus-basic --contract "BasicTypes" --test-directory "./test/" --inheritance-path "../src/" --fuzzer echidna ``` -Running this command should generate a `BasicTypes_Echidna_Test.sol` file in the [tests](/test/test/) directory of the Foundry project. - -## Supported input types - -The tool currently supports all elementary types (uint*, int*, bool, address, bytes, bytes*, string), one-dimensional fixed-size and dynamic arrays, structs, and enums. Any other types may or may not work. +Running this command should generate a `BasicTypes_Echidna_Test.sol` file in the [tests](/tests/test_data/test/) directory of the Foundry project. ## Command-line options @@ -38,3 +42,9 @@ Additional options are available for the script: - `-td`/`--test-directory` `path_to_test_directory`: The path to the test directory relative to the working directory. - `-i`/`--inheritance-path` `relative_path_to_contract`: The relative path from the test directory to the contract (used for inheritance). - `-f`/`--fuzzer` `fuzzer_name`: The name of the fuzzer, currently supported: `echidna` and `medusa` + +## Contributing +For information about how to contribute to this project, check out the [CONTRIBUTING](CONTRIBUTING.md) guidelines. + +## License +test-generator is licensed and distributed under the [AGPLv3](LICENSE). diff --git a/test_generator/main.py b/test_generator/main.py index 41c4d5b..3a807f5 100644 --- a/test_generator/main.py +++ b/test_generator/main.py @@ -5,6 +5,8 @@ import argparse import jinja2 +from pkg_resources import require + from slither import Slither from slither.core.declarations.contract import Contract from test_generator.utils.crytic_print import CryticPrint @@ -109,6 +111,12 @@ def main() -> None: dest="selected_fuzzer", help="Define the fuzzer used. Valid inputs: 'echidna', 'medusa'", ) + parser.add_argument( + "--version", + help="displays the current version", + version=require("test-generator")[0].version, + action="version", + ) args = parser.parse_args() file_path = args.file_path