├── .editorconfig ├── .flake8 ├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── publish_pypi.yml │ ├── sync-jira.yml │ ├── test-build-docs.yml │ ├── test-build-package.yml │ └── test-python.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── all_local_packages.txt ├── conftest.py ├── docs ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Makefile ├── _static │ ├── espressif-logo.svg │ └── theme_overrides.css ├── _templates │ └── layout.html ├── apis │ ├── pytest-embedded-arduino.rst │ ├── pytest-embedded-idf.rst │ ├── pytest-embedded-jtag.rst │ ├── pytest-embedded-nuttx.rst │ ├── pytest-embedded-qemu.rst │ ├── pytest-embedded-serial-esp.rst │ ├── pytest-embedded-serial.rst │ ├── pytest-embedded-wokwi.rst │ └── pytest-embedded.rst ├── concepts │ ├── key-concepts.md │ └── services.md ├── conf.py ├── index.rst ├── requirements.txt └── usages │ ├── expecting.rst │ └── markers.rst ├── examples ├── .gitignore ├── arduino │ ├── README.md │ ├── hello_world │ │ ├── hello_world.ino │ │ └── test_hello_world.py │ └── pytest.ini ├── echo │ ├── README.md │ ├── conftest.py │ ├── pytest.ini │ └── test_echo.py ├── esp-idf-qemu │ ├── README.md │ ├── hello_world │ │ ├── CMakeLists.txt │ │ ├── main │ │ │ ├── CMakeLists.txt │ │ │ └── hello_world_main.c │ │ └── test_hello_world.py │ └── pytest.ini ├── esp-idf │ ├── README.md │ ├── hello_world │ │ ├── CMakeLists.txt │ │ ├── main │ │ │ ├── CMakeLists.txt │ │ │ └── hello_world_main.c │ │ └── test_hello_world.py │ └── pytest.ini └── nuttx-esp │ ├── README.md │ ├── pytest.ini │ └── test_nuttx_esp.py ├── foreach.sh ├── pyproject.toml ├── pytest-embedded-arduino ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_arduino │ ├── __init__.py │ ├── app.py │ └── serial.py └── tests │ └── test_arduino.py ├── pytest-embedded-idf ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_idf │ ├── __init__.py │ ├── app.py │ ├── dut.py │ ├── linux.py │ ├── serial.py │ ├── unity_tester.py │ └── utils.py └── tests │ └── test_idf.py ├── pytest-embedded-jtag ├── LICENSE ├── README.md ├── __init__.py ├── pyproject.toml ├── pytest_embedded_jtag │ ├── __init__.py │ ├── _telnetlib │ │ ├── LICENSE │ │ ├── __init__.py │ │ └── telnetlib.py │ ├── gdb.py │ └── openocd.py └── tests │ └── test_jtag.py ├── pytest-embedded-nuttx ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_nuttx │ ├── __init__.py │ ├── app.py │ ├── dut.py │ ├── qemu.py │ └── serial.py └── tests │ └── test_nuttx.py ├── pytest-embedded-qemu ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_qemu │ ├── __init__.py │ ├── app.py │ ├── dut.py │ └── qemu.py └── tests │ └── test_qemu.py ├── pytest-embedded-serial-esp ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_serial_esp │ ├── __init__.py │ └── serial.py └── tests │ └── test_esp.py ├── pytest-embedded-serial ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_serial │ ├── __init__.py │ ├── dut.py │ └── serial.py └── tests │ └── test_serial.py ├── pytest-embedded-wokwi ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded_wokwi │ ├── __init__.py │ ├── arduino.py │ ├── dut.py │ ├── idf.py │ └── wokwi_cli.py └── tests │ └── test_wokwi.py ├── pytest-embedded ├── LICENSE ├── README.md ├── pyproject.toml ├── pytest_embedded │ ├── __init__.py │ ├── app.py │ ├── dut.py │ ├── dut_factory.py │ ├── log.py │ ├── plugin.py │ ├── unity.py │ └── utils.py └── tests │ └── test_base.py ├── requirements.txt ├── ruff.toml └── tests ├── esp-idf ├── components │ └── soc │ │ ├── esp32 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32c2 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32c3 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32c5 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32c6 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32c61 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32h2 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32h21 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32p4 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ ├── esp32s2 │ │ └── include │ │ │ └── soc │ │ │ └── soc_caps.h │ │ └── esp32s3 │ │ └── include │ │ └── soc │ │ └── soc_caps.h └── tools │ ├── cmake │ └── version.cmake │ └── idf_py_actions │ └── constants.py └── fixtures ├── esp32_qemu.bin ├── gdb_panic_server.py ├── gen_esp32part.py ├── hello_world_arduino ├── build │ ├── build.options.json │ ├── hello_world_arduino.ino.bin │ ├── hello_world_arduino.ino.bootloader.bin │ ├── hello_world_arduino.ino.elf │ └── hello_world_arduino.ino.partitions.bin └── esp32.diagram.json ├── hello_world_esp32 ├── build │ ├── bootloader │ │ └── bootloader.bin │ ├── config │ │ └── sdkconfig.json │ ├── flasher_args.json │ ├── hello_world.bin │ ├── hello_world.elf │ └── partition_table │ │ └── partition-table.bin └── gdbinit ├── hello_world_esp32_flash_enc ├── build │ ├── bootloader │ │ └── bootloader.bin │ ├── config │ │ └── sdkconfig.json │ ├── flasher_args.json │ ├── hello_world.bin │ └── partition_table │ │ └── partition-table.bin ├── pre_encryption_efuses.bin └── pre_encryption_key.bin ├── hello_world_esp32c3 └── build │ ├── bootloader │ └── bootloader.bin │ ├── config │ └── sdkconfig.json │ ├── flasher_args.json │ ├── hello_world.bin │ ├── hello_world.elf │ └── partition_table │ └── partition-table.bin ├── hello_world_esp32c3_panic ├── build │ ├── bootloader │ │ └── bootloader.bin │ ├── config │ │ └── sdkconfig.json │ ├── flasher_args.json │ ├── hello_world.bin │ ├── hello_world.elf │ ├── hello_world.map │ ├── partition_table │ │ └── partition-table.bin │ └── prefix_map_gdbinit └── main │ └── hello_world_main.c ├── hello_world_linux └── build │ ├── config │ └── sdkconfig.json │ ├── hello_world.elf │ └── project_description.json ├── hello_world_nuttx └── nuttx.bin ├── hello_world_nuttx_mcuboot ├── mcuboot-esp32.bin └── nuttx.bin ├── hello_world_nuttx_qemu ├── nuttx.merged.bin └── qemu_efuse.bin ├── unit_test_app_esp32 ├── CMakeLists.txt ├── build │ ├── bootloader │ │ └── bootloader.bin │ ├── case_tester_example.bin │ ├── config │ │ └── sdkconfig.json │ ├── flasher_args.json │ └── partition_table │ │ └── partition-table.bin └── main │ ├── CMakeLists.txt │ ├── case_tester_example.c │ └── test_app_main.c ├── unit_test_app_esp32c3 ├── CMakeLists.txt ├── build │ ├── bootloader │ │ └── bootloader.bin │ ├── case_tester_example.bin │ ├── config │ │ └── sdkconfig.json │ ├── flasher_args.json │ └── partition_table │ │ └── partition-table.bin └── main │ ├── CMakeLists.txt │ ├── case_tester_example.c │ └── test_app_main.c └── unit_test_app_linux └── build ├── config └── sdkconfig.json ├── project_description.json └── test_esp_event.elf /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [{*.md,*.rst}] 16 | trim_trailing_whitespace = false 17 | indent_size = 2 18 | 19 | [*.py] 20 | max_line_length = 119 21 | 22 | [{*.sh,*.yml,*.yaml}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | extend-ignore = E203 4 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # ruff, ruff-format 2 | 3f0008207d272b7fa4329d86a157f4533f1a441f 3 | a90b0d0c35a8e1c6896130e23c46cedee48b4158 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Configuration Files** 14 | - [ ] `pytest.ini` 15 | - [ ] `conftest.py` 16 | - [ ] `pyproject.toml` 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Run `pytest ...` 21 | 2. Got error message "..." 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Dev Environment (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Python Version [e.g. 3.8.5] 29 | - Pytest Version [e.g. 6.1.2] 30 | - pytest-embedded Version [e.g. 1.0.0] 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | analyze: 9 | name: Analyze 10 | runs-on: ubuntu-latest 11 | permissions: 12 | security-events: write 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | language: [ 'python' ] 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | - name: Initialize CodeQL 21 | uses: github/codeql-action/init@v3 22 | with: 23 | languages: ${{ matrix.language }} 24 | - name: Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v3 26 | with: 27 | category: "/language:${{matrix.language}}" 28 | -------------------------------------------------------------------------------- /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.7" 17 | - name: Build packages 18 | run: bash foreach.sh build 19 | - name: Publish packages 20 | env: 21 | FLIT_USERNAME: __token__ 22 | FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 23 | run: bash foreach.sh publish 24 | -------------------------------------------------------------------------------- /.github/workflows/sync-jira.yml: -------------------------------------------------------------------------------- 1 | # FILE: .github/workflows/sync-jira.yml 2 | --- 3 | # This GitHub Actions workflow synchronizes GitHub issues, comments, and pull requests with Jira. 4 | # It triggers on new issues, issue comments, and on a scheduled basis. 5 | # The workflow uses a custom action to perform the synchronization with Jira (espressif/sync-jira-actions). 6 | 7 | name: 🔷 Sync to Jira 8 | 9 | run-name: > 10 | Sync to Jira - 11 | ${{ github.event_name == 'issue_comment' && 'Issue Comment' || 12 | github.event_name == 'schedule' && 'New Pull Requests' || 13 | github.event_name == 'issues' && 'New Issue' || 14 | github.event_name == 'workflow_dispatch' && 'Manual Sync' }} 15 | 16 | on: 17 | issues: { types: [opened] } 18 | issue_comment: { types: [created, edited, deleted] } 19 | schedule: [cron: "0 * * * *"] 20 | workflow_dispatch: 21 | inputs: 22 | action: 23 | { 24 | description: "Action to be performed", 25 | required: true, 26 | default: "mirror-issues", 27 | } 28 | issue-numbers: 29 | { 30 | description: "Issue numbers to sync (comma-separated)", 31 | required: true, 32 | } 33 | 34 | jobs: 35 | sync-to-jira: 36 | name: > 37 | Sync to Jira - 38 | ${{ github.event_name == 'issue_comment' && 'Issue Comment' || 39 | github.event_name == 'schedule' && 'New Pull Requests' || 40 | github.event_name == 'issues' && 'New Issue' || 41 | github.event_name == 'workflow_dispatch' && 'Manual Sync' }} 42 | runs-on: ubuntu-latest 43 | permissions: 44 | contents: read 45 | issues: write 46 | pull-requests: write 47 | steps: 48 | - name: Check out 49 | uses: actions/checkout@v4 50 | 51 | - name: Run synchronization to Jira 52 | uses: espressif/sync-jira-actions@v1 53 | with: 54 | cron_job: ${{ github.event_name == 'schedule' && 'true' || '' }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | JIRA_PASS: ${{ secrets.JIRA_PASS }} 58 | JIRA_PROJECT: RDT 59 | JIRA_COMPONENT: pytest-embedded 60 | JIRA_URL: ${{ secrets.JIRA_URL }} 61 | JIRA_USER: ${{ secrets.JIRA_USER }} 62 | -------------------------------------------------------------------------------- /.github/workflows/test-build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Test Build Docs 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "**.py" 7 | - "docs/**" 8 | - "*.md" 9 | 10 | jobs: 11 | test-build-docs: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.7" 19 | - name: Install dependencies 20 | run: | 21 | bash foreach.sh install 22 | pip install -r docs/requirements.txt 23 | - name: Test build docs 24 | run: | 25 | cd docs && make html SPHINXOPTS="-W" 26 | -------------------------------------------------------------------------------- /.github/workflows/test-build-package.yml: -------------------------------------------------------------------------------- 1 | name: Test Build Python Packages 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test-build-packages: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: "3.7" 15 | - name: Build packages 16 | run: | 17 | bash foreach.sh build 18 | -------------------------------------------------------------------------------- /.github/workflows/test-python.yml: -------------------------------------------------------------------------------- 1 | name: Test Python Packages 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "**.py" 7 | - .github/workflows/test-python.yml 8 | - "!docs/**" 9 | push: 10 | branches: [main] 11 | 12 | # cancel jobs if new commit pushed 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | test-python: 19 | timeout-minutes: 40 20 | strategy: 21 | matrix: 22 | include: 23 | - python-version: "3.7" 24 | arch: "ARM64" 25 | - python-version: "3.13" 26 | arch: "X64" 27 | fail-fast: false 28 | runs-on: 29 | - self-hosted 30 | - multiboard 31 | - ${{ matrix.arch }} 32 | container: 33 | image: python:${{ matrix.python-version }} 34 | options: --privileged 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Install dependencies 38 | run: | 39 | apt update && apt install -y socat zip 40 | pip install -U pip 41 | export PIP_EXTRA_INDEX_URL="https://dl.espressif.com/pypi" 42 | pip install cryptography --prefer-binary 43 | pip install -r requirements.txt 44 | bash foreach.sh install 45 | - name: Check ports 46 | run: ls -la /dev/ttyUSB* 47 | - name: Run Tests with coverage 48 | run: | 49 | pytest \ 50 | --junitxml=pytest.xml \ 51 | --cov-report=term-missing \ 52 | --cov=pytest_embedded \ 53 | --cov=pytest_embedded_arduino \ 54 | --cov=pytest_embedded_idf \ 55 | --cov=pytest_embedded_jtag \ 56 | --cov=pytest_embedded_qemu \ 57 | --cov=pytest_embedded_serial \ 58 | --cov=pytest_embedded_serial_esp 59 | - name: Zip log files 60 | if: failure() 61 | run: | 62 | zip -r logs.zip /tmp/pytest-embedded 63 | - uses: actions/upload-artifact@v4 64 | if: failure() 65 | with: 66 | name: logs-${{ matrix.python-version }} 67 | path: logs.zip 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | .pytest_cache 47 | report.xml 48 | *.log 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # sphinx documentation 55 | docs/_build 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # IPython Notebook 61 | .ipynb_checkpoints 62 | 63 | # pyenv 64 | .python-version 65 | 66 | # venv 67 | venv/ 68 | .venv/ 69 | 70 | # IDE 71 | .idea/ 72 | 73 | # built binaries 74 | tests/fixtures/unit_test_app*/build/ 75 | !tests/fixtures/**/bootloader.bin 76 | !tests/fixtures/**/partition-table.bin 77 | !tests/fixtures/**/sdkconfig.json 78 | !tests/fixtures/**/flasher_args.json 79 | !tests/fixtures/unit_test_app_*/build/**/case_tester_example.bin 80 | 81 | sdkconfig 82 | sdkconfig.old 83 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | exclude: | 5 | (?x)^( 6 | .*tests/fixtures/.+| 7 | )$ 8 | 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v5.0.0 12 | hooks: 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | args: ["-f=lf"] 17 | - repo: https://github.com/astral-sh/ruff-pre-commit 18 | rev: "v0.11.6" 19 | hooks: 20 | - id: ruff 21 | args: ["--fix"] 22 | - id: ruff-format 23 | args: ["--preview"] 24 | 25 | # documentation 26 | - repo: https://github.com/sphinx-contrib/sphinx-lint 27 | rev: v1.0.0 28 | hooks: 29 | - id: sphinx-lint 30 | args: [--enable=default-role] 31 | - repo: https://github.com/hfudev/rstfmt 32 | rev: v0.1.4 33 | hooks: 34 | - id: rstfmt 35 | args: ["-w", "-1"] 36 | files: \.rst$ 37 | 38 | # git commit 39 | - repo: https://github.com/espressif/conventional-precommit-linter 40 | rev: v1.10.0 41 | hooks: 42 | - id: conventional-precommit-linter 43 | stages: [commit-msg] 44 | args: 45 | ["--types=chore,change,ci,docs,feat,fix,refactor,remove,revert,bump"] 46 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | sphinx: 8 | # Path to your Sphinx configuration file. 9 | configuration: docs/conf.py 10 | 11 | build: 12 | os: ubuntu-22.04 13 | tools: 14 | python: "3.7" 15 | 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | - requirements: all_local_packages.txt 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Workflow 4 | 5 | We use [pre-commit](https://pre-commit.com) for code formatting, and some linter checks. You can install it by 6 | 7 | ```shell 8 | $ pre-commit install -t pre-commit -t commit-msg 9 | ``` 10 | 11 | We use [commitizen](https://github.com/commitizen-tools/commitizen) to auto generate the [CHANGELOG.md](./CHANGELOG.md). 12 | You don't need to install it or know anything about the tool itself, but please follow 13 | the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) rule when writing commit messages. 14 | 15 | ## Install Virtual Environment 16 | 17 | We recommend using virtual environment for development. You can create one by 18 | 19 | ```shell 20 | $ python -m venv .venv 21 | $ source .venv/bin/activate 22 | ``` 23 | 24 | ## Install Dependencies 25 | 26 | We recommend to install the local subpackages in editable mode. 27 | 28 | ```shell 29 | $ pip install -r requirements.txt 30 | $ bash foreach.sh install-editable 31 | ``` 32 | 33 | ## Running Tests 34 | 35 | By default, all tests under all plugins would be run. 36 | 37 | ```shell 38 | $ # export DONT_SKIP_JTAG_TESTS=1 (when you have a jtag connection) 39 | $ pytest 40 | ``` 41 | 42 | ## Writing Tests 43 | 44 | Basically we're following 45 | the [official documentation](https://docs.pytest.org/en/stable/writing_plugins.html#testing-plugins). You could also 46 | refer to the tests under each plugin. 47 | 48 | ## Building Docs 49 | 50 | We use `sphinx` with espressif theme to build the docs. The docstring is written in `Google` style. Here's an [example](https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html). 51 | 52 | For building locally, you need to install the dependencies first. 53 | 54 | ```shell 55 | $ pip install -r docs/requirements.txt 56 | ``` 57 | 58 | Then you can build the docs by 59 | 60 | ```shell 61 | $ cd docs 62 | $ make html 63 | ``` 64 | 65 | For documentation preview, you may use any browser you prefer. The executable has to be searchable in `PATH`. For example we're using firefox here. 66 | 67 | ```shell 68 | $ firefox _build/html/index.html 69 | ``` 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This repository contains files licensed under multiple licenses. The licensing terms are as follows: 2 | 3 | 1. Files in the examples/ folder 4 | The files within the examples/ folder are licensed under the Creative Commons Zero v1.0 Universal (CC0-1.0) License: https://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | 2. Files in Python packages 7 | Each Python package in this repository is licensed separately. Please refer to the LICENSE file located in each package for specific licensing terms. 8 | 9 | 3. All other files 10 | Unless otherwise specified, all other files in this repository are licensed under the MIT License: 11 | 12 | MIT License 13 | 14 | Copyright (c) 2024 Espressif Systems (Shanghai) Co. Ltd. 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-embedded 2 | 3 | [![Documentation Status](https://readthedocs.com/projects/espressif-pytest-embedded/badge/?version=latest)](https://docs.espressif.com/projects/pytest-embedded/en/latest/?badge=latest) ![Python 3.7+](https://img.shields.io/pypi/pyversions/pytest-embedded) 4 | 5 | A pytest plugin that has multiple services available for various functionalities. Designed for embedded testing. 6 | 7 | ## Installation 8 | 9 | [![pytest-embedded](https://img.shields.io/pypi/v/pytest-embedded?color=green&label=pytest-embedded)](https://pypi.org/project/pytest-embedded/) 10 | [![pytest-embedded-serial](https://img.shields.io/pypi/v/pytest-embedded-serial?color=green&label=pytest-embedded-serial)](https://pypi.org/project/pytest-embedded-serial/) 11 | [![pytest-embedded-serial-esp](https://img.shields.io/pypi/v/pytest-embedded-serial-esp?color=green&label=pytest-embedded-serial-esp)](https://pypi.org/project/pytest-embedded-serial-esp/) 12 | [![pytest-embedded-idf](https://img.shields.io/pypi/v/pytest-embedded-idf?color=green&label=pytest-embedded-idf)](https://pypi.org/project/pytest-embedded-idf/) 13 | [![pytest-embedded-qemu](https://img.shields.io/pypi/v/pytest-embedded-qemu?color=green&label=pytest-embedded-qemu)](https://pypi.org/project/pytest-embedded-qemu/) 14 | [![pytest-embedded-arduino](https://img.shields.io/pypi/v/pytest-embedded-arduino?color=green&label=pytest-embedded-arduino)](https://pypi.org/project/pytest-embedded-arduino/) 15 | [![pytest-embedded-wokwi](https://img.shields.io/pypi/v/pytest-embedded-wokwi?color=green&label=pytest-embedded-wokwi)](https://pypi.org/project/pytest-embedded-wokwi/) 16 | [![pytest-embedded-nuttx](https://img.shields.io/pypi/v/pytest-embedded-nuttx?color=green&label=pytest-embedded-nuttx)](https://pypi.org/project/pytest-embedded-nuttx/) 17 | 18 | Packages under this repo mainly use semantic versioning. Sometimes a bug fix version may contain some non-breaking new features as well. 19 | 20 | It is recommended to use `~=1.0` to get rid of breaking changes, and use the latest new features. For example, 21 | 22 | ```shell 23 | pip install -U pytest-embedded~=1.0 24 | ``` 25 | 26 | ## Quickstart 27 | 28 | - `pip install -U pytest-embedded~=1.0` 29 | - Create a file `test_basic.py` 30 | 31 | ```python 32 | from pytest_embedded import Dut 33 | 34 | 35 | def test_basic_expect(redirect, dut: Dut): 36 | with redirect(): 37 | print('this would be redirected') 38 | 39 | dut.expect('this') 40 | dut.expect_exact('would') 41 | dut.expect('[be]{2}') 42 | dut.expect_exact('redirected') 43 | ``` 44 | 45 | - Run the test with `pytest`, the result would be like: 46 | 47 | ```shell 48 | collected 1 item 49 | 50 | test_basic.py . [100%] 51 | 52 | ============================= 1 passed in 0.01s ============================= 53 | ``` 54 | 55 | - if run with `pytest -s`, the output would be as follows: 56 | 57 | ```shell 58 | collected 1 item 59 | 60 | test_basic.py 2022-01-01 12:34:56 this would be redirected 61 | . 62 | 63 | ============================= 1 passed in 0.01s ============================= 64 | ``` 65 | 66 | The `print` line is also duplicated to console output. 67 | 68 | ## Extra Services 69 | 70 | You can activate more services with `pytest --embedded-services service[,service]` to enable extra fixtures and functionalities. These services are provided by several optional dependencies. You can install them via `pip` as well. 71 | 72 | Available services: 73 | 74 | - `serial`: serial port utilities. 75 | - `esp`: auto-detect target/port by [esptool](https://github.com/espressif/esptool). 76 | - `idf`: auto-detect more app info with [ESP-IDF](https://github.com/espressif/esp-idf) specific rules, auto-flash the binary into the target. 77 | - `jtag`: openocd/gdb utilities 78 | - `qemu`: running test cases on QEMU instead of the real target. 79 | - `arduino`: auto-detect more app info with [arduino](https://github.com/arduino/Arduino) specific rules, auto-flash the binary into the target. 80 | - `wokwi`: running test cases with [Wokwi](https://wokwi.com/) instead of the real target. 81 | - `nuttx`: service for [nuttx](https://nuttx.apache.org/) project, optionally with espressif devices. 82 | 83 | ## Resources 84 | 85 | - Documentation is hosted at [https://docs.espressif.com/projects/pytest-embedded/en/latest/](https://docs.espressif.com/projects/pytest-embedded/en/latest/) 86 | - More examples under [examples](https://github.com/espressif/pytest-embedded/tree/main/examples) 87 | -------------------------------------------------------------------------------- /all_local_packages.txt: -------------------------------------------------------------------------------- 1 | -e ./pytest-embedded/ 2 | -e ./pytest-embedded-serial/ 3 | -e ./pytest-embedded-serial-esp/ 4 | -e ./pytest-embedded-idf/ 5 | -e ./pytest-embedded-jtag/ 6 | -e ./pytest-embedded-qemu/ 7 | -e ./pytest-embedded-arduino/ 8 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import textwrap 5 | from typing import List, Pattern 6 | 7 | import pytest 8 | from _pytest.config import Config 9 | from _pytest.fixtures import FixtureRequest 10 | from _pytest.legacypath import Testdir 11 | 12 | pytest_plugins = [ 13 | 'pytester', 14 | ] 15 | 16 | 17 | @pytest.fixture(autouse=True) 18 | def copy_fixtures(testdir: Testdir): 19 | fixture_dir = os.path.join(os.path.dirname(__file__), 'tests', 'fixtures') 20 | for item in os.listdir(fixture_dir): 21 | if item == '__pycache__': 22 | continue 23 | 24 | if os.path.isfile(os.path.join(fixture_dir, item)): 25 | shutil.copy(os.path.join(fixture_dir, item), os.path.join(str(testdir.tmpdir), item)) 26 | else: 27 | shutil.copytree(os.path.join(fixture_dir, item), os.path.join(str(testdir.tmpdir), item)) 28 | 29 | testdir.makepyprojecttoml( 30 | textwrap.dedent(""" 31 | [tool.pytest.ini_options] 32 | filterwarnings = "ignore:FutureWarning" 33 | junit_family = "xunit1" 34 | """) 35 | ) 36 | 37 | yield 38 | 39 | 40 | @pytest.fixture() 41 | def copy_mock_esp_idf(testdir: Testdir): 42 | esp_idf = os.path.join(os.path.dirname(__file__), 'tests', 'esp-idf') 43 | for item in os.listdir(esp_idf): 44 | if os.path.isfile(os.path.join(esp_idf, item)): 45 | shutil.copy(os.path.join(esp_idf, item), os.path.join(str(testdir.tmpdir), item)) 46 | else: 47 | shutil.copytree(os.path.join(esp_idf, item), os.path.join(str(testdir.tmpdir), item)) 48 | 49 | yield 50 | 51 | 52 | @pytest.fixture(autouse=True) 53 | def cache_file_remove(cache_dir): 54 | yield 55 | _cache_file_path = os.path.join(cache_dir, 'port_target_cache') 56 | if os.path.exists(_cache_file_path): 57 | os.remove(_cache_file_path) 58 | 59 | 60 | @pytest.fixture 61 | def first_index_of_messages(): 62 | def _fake(_pattern: Pattern, _messages: List[str], _start: int = 0) -> int: 63 | for i, _message in enumerate(_messages): 64 | if _pattern.match(_message) and i >= _start: 65 | return i 66 | 67 | raise AssertionError(f'Not found {_pattern.pattern}') 68 | 69 | return _fake 70 | 71 | 72 | @pytest.fixture(autouse=True) 73 | def temp_disable_packages(monkeypatch, request: FixtureRequest): 74 | temp_marker = request.node.get_closest_marker('temp_disable_packages') 75 | if not temp_marker: 76 | return 77 | 78 | packages = temp_marker.args 79 | for name in list(sys.modules): 80 | if name in packages or name.split('.')[0] in packages: 81 | monkeypatch.setitem(sys.modules, name, None) 82 | 83 | 84 | def pytest_configure(config: Config) -> None: 85 | for name, description in {'temp_disable_packages': 'disable packages in function scope'}.items(): 86 | config.addinivalue_line('markers', f'{name}: {description}') 87 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CHANGELOG.md 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CONTRIBUTING.md 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/espressif-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 16 | 18 | 23 | 26 | 27 | 28 | 33 | 38 | 39 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 62 | 69 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/_static/theme_overrides.css: -------------------------------------------------------------------------------- 1 | /* override table width restrictions */ 2 | @media screen and (min-width: 767px) { 3 | 4 | .wy-table-responsive table td { 5 | /* !important prevents the common CSS stylesheets from overriding 6 | this as on RTD they are loaded after this stylesheet */ 7 | white-space: normal !important; 8 | } 9 | 10 | .wy-table-responsive { 11 | overflow: visible !important; 12 | } 13 | } 14 | 15 | .wy-side-nav-search { 16 | background-color: #e3e3e3 !important; 17 | } 18 | 19 | .wy-side-nav-search input[type=text] { 20 | border-radius: 0px !important; 21 | border-color: #333333 !important; 22 | } 23 | 24 | .icon-home { 25 | color: #333333 !important; 26 | } 27 | 28 | .icon-home:hover { 29 | background-color: #d6d6d6 !important; 30 | } 31 | 32 | .version { 33 | color: #000000 !important; 34 | } 35 | 36 | a:hover { 37 | color: #bd2c2a !important; 38 | } 39 | 40 | .logo { 41 | width: 240px !important; 42 | } 43 | 44 | .highlight .c1 { 45 | color: #008080; 46 | } 47 | 48 | .bolditalics { 49 | font-weight: bold; 50 | font-style: italic; 51 | } 52 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block document %} 4 | 5 | {{ super() }} 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-arduino.rst: -------------------------------------------------------------------------------- 1 | ######################### 2 | pytest-embedded-arduino 3 | ######################### 4 | 5 | .. automodule:: pytest_embedded_arduino.app 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_arduino.serial 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-idf.rst: -------------------------------------------------------------------------------- 1 | ##################### 2 | pytest-embedded-idf 3 | ##################### 4 | 5 | .. automodule:: pytest_embedded_idf.app 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_idf.dut 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: pytest_embedded_idf.linux 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: pytest_embedded_idf.serial 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | .. automodule:: pytest_embedded_idf.unity_tester 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-jtag.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | pytest-embedded-jtag 3 | ###################### 4 | 5 | .. automodule:: pytest_embedded_jtag.gdb 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_jtag.openocd 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-nuttx.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | pytest-embedded-nuttx 3 | ####################### 4 | 5 | .. automodule:: pytest_embedded_nuttx.app 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_nuttx.dut 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: pytest_embedded_nuttx.serial 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-qemu.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | pytest-embedded-qemu 3 | ###################### 4 | 5 | .. automodule:: pytest_embedded_qemu.app 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_qemu.dut 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: pytest_embedded_qemu.qemu 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-serial-esp.rst: -------------------------------------------------------------------------------- 1 | ############################ 2 | pytest-embedded-serial-esp 3 | ############################ 4 | 5 | .. automodule:: pytest_embedded_serial_esp.serial 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-serial.rst: -------------------------------------------------------------------------------- 1 | ######################## 2 | pytest-embedded-serial 3 | ######################## 4 | 5 | .. automodule:: pytest_embedded_serial.dut 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_serial.serial 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded-wokwi.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | pytest-embedded-wokwi 3 | ####################### 4 | 5 | .. automodule:: pytest_embedded_wokwi.arduino 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded_wokwi.dut 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: pytest_embedded_wokwi.idf 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: pytest_embedded_wokwi.wokwi_cli 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/apis/pytest-embedded.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | pytest-embedded 3 | ################# 4 | 5 | .. automodule:: pytest_embedded.app 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | 10 | .. automodule:: pytest_embedded.dut 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: pytest_embedded.dut_factory 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: pytest_embedded.log 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | .. automodule:: pytest_embedded.plugin 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | .. automodule:: pytest_embedded.unity 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | .. automodule:: pytest_embedded.utils 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/concepts/key-concepts.md: -------------------------------------------------------------------------------- 1 | # Key Concepts 2 | 3 | ## Fixtures 4 | 5 | Each test case would initialize a few fixtures. The most important fixtures are: 6 | 7 | - `msg_queue`, a message queue. There's a background listener process to read all the messages from this queue, log them to the terminal with an optional timestamp, and record them to the pexpect process. 8 | - `pexpect_proc`, a pexpect process, that could run `pexpect.expect()` for testing purposes. 9 | - `app`, the built binary 10 | - `dut`, Device Under Test (DUT) 11 | 12 | A DUT would contain several daemon processes/threads. The output of each of them would be redirected to the `msg_queue` fixture. 13 | 14 | ```{eval-rst} 15 | .. note:: 16 | 17 | You may redirect any output to the ``msg_queue`` fixture by ``contextlib.redirect_stdout``. 18 | 19 | .. code-block:: python 20 | 21 | import contextlib 22 | 23 | def test_redirect(dut, msg_queue): 24 | with contextlib.redirect_stdout(msg_queue): 25 | print('will be redirected') 26 | 27 | dut.expect_exact('redirected') 28 | 29 | Or you may redirect the output from a fixture `redirect` 30 | 31 | .. code-block:: python 32 | 33 | def test_redirect(dut, msg_queue, redirect): 34 | with redirect(): 35 | print('will also be redirected') 36 | 37 | dut.expect_exact('redirected') 38 | 39 | ``` 40 | 41 | You can run `pytest --fixtures` to get all the fixtures defined with `pytest-embedded`. They are under the section `fixtures defined from pytest_embedded.plugin`. 42 | 43 | ## Parametrization 44 | 45 | All the CLI options support parametrization via `indirect=True`. Parametrization is a feature provided by `pytest`, please refer to [Parametrizing tests](https://docs.pytest.org/en/latest/example/parametrize.html) for its documentation. 46 | 47 | For example, running shell command `pytest` with the test script: 48 | 49 | ```python 50 | @pytest.mark.parametrize( 51 | 'embedded_services, app_path', 52 | [ 53 | ('idf', app_path_1), 54 | ('idf', app_path_2), 55 | ], 56 | indirect=True, 57 | ) 58 | def test_serial_tcp(dut): 59 | assert dut.app.target == 'esp32' 60 | dut.expect('Restart now') 61 | ``` 62 | 63 | is equivalent to running two shell commands `pytest --embedded-services idf --app-path ` and `pytest --embedded-services idf --app-path ` with the test script: 64 | 65 | ```python 66 | def test_serial_tcp(dut): 67 | assert dut.app.target == 'esp32' 68 | dut.expect('Restart now') 69 | ``` 70 | 71 | ## Services 72 | 73 | You can activate more services with `pytest --embedded-services service[,service]` to enable extra fixtures and functionalities. These services are provided by several optional dependencies. You can install them via `pip` as well. 74 | 75 | Available services: 76 | 77 | - `serial`: serial port utilities. 78 | - `esp`: auto-detect target/port by [esptool](https://github.com/espressif/esptool). 79 | - `idf`: auto-detect more app info with [ESP-IDF](https://github.com/espressif/esp-idf) specific rules, auto-flash the binary into the target. 80 | - `jtag`: openocd/gdb utilities. 81 | - `qemu`: running test cases on QEMU instead of the real target. 82 | - `arduino`: auto-detect more app info with [arduino](https://github.com/arduino/Arduino) specific rules, auto-flash the binary into the target. 83 | - `wokwi`: running test cases with [Wokwi](https://wokwi.com/) instead of the real target. 84 | 85 | ## Multi DUTs 86 | 87 | Sometimes we need multi DUTs while testing, e.g., master-slave or mesh testing. 88 | 89 | Here are a few examples of how to enable this. For detailed information, please refer to the `embedded` group of the `pytest --help`. 90 | 91 | ### Enable multi DUTs by specifying `--count` 92 | 93 | After you enabled the multi-dut mode, all the fixtures would be a tuple with instances. Each instance inside the tuple would be independent. For parametrization, each configuration will use `|` as a separator for each instance. 94 | 95 | For example, running shell command: 96 | 97 | ```shell 98 | pytest \ 99 | --embedded-services serial|serial \ 100 | --count 2 \ 101 | --app-path | 102 | ``` 103 | 104 | would enable 2 DUTs with `serial` service. `app` would be a tuple of 2 `App` instances, and `dut` would be a tuple of 2 `Dut` Instances. 105 | 106 | You can test with: 107 | 108 | ```python 109 | def test(dut): 110 | master = dut[0] 111 | slave = dut[1] 112 | 113 | master.expect('sent') 114 | slave.expect('received') 115 | ``` 116 | 117 | ### Specify once when applying to all DUTs 118 | 119 | If all DUTs share the same configuration value, you can specify only once. 120 | 121 | ```shell 122 | pytest \ 123 | --embedded-services serial \ 124 | --count 2 \ 125 | --app-path | 126 | ``` 127 | 128 | `--embedded-services serial` would apply to all DUTs 129 | 130 | ### Vacant Value if it's Useless 131 | 132 | Sometimes one option is only useful when enabling specific services. You can set a vacant value if this config is only useful for certain DUTs. 133 | 134 | ```shell 135 | pytest \ 136 | --embedded-services qemu|serial \ 137 | --count 2 \ 138 | --app-path | \ 139 | --qemu-cli-args "|" \ 140 | --port "|" 141 | ``` 142 | 143 | `--qemu-cli-args` would apply to the first DUT with `qemu` service and `--port` would apply to the second DUT with `serial` service. 144 | 145 | ## Logging 146 | 147 | `pytest-embedded` print all the DUT output with the timestamp. If you want to remove the timestamp, please run pytest with `pytest --with-timestamp n` to disable this feature. 148 | 149 | By default, `pytest` would swallow the stdout. If you want to check the live output, please run pytest with `pytest -s`. 150 | -------------------------------------------------------------------------------- /docs/concepts/services.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | ## Fixtures 4 | 5 | Please refer to instructions under the "fixtures defined from pytest_embedded.plugin" chapter of the output of `pytest --fixtures`. 6 | 7 | ## CLI Options 8 | 9 | Please refer to the instructions under the "embedded" or "embedded-[SERVICE]" groups of the output of `pytest --help`. 10 | 11 | ## Dependency Graph for Services 12 | 13 | The arrow points from the dependent service to the service it depends on. For example, `pytest-embedded-serial-esp` depends on `pytest-embedded-serial`. 14 | 15 | ```{mermaid} 16 | graph LR 17 | pytest-embedded-serial 18 | 19 | pytest-embedded-nuttx --> pytest-embedded-serial 20 | pytest-embedded-nuttx -->|optional, support test on espressif chips| pytest-embedded-serial-esp 21 | pytest-embedded-nuttx -->|optional, support test on qemu| pytest-embedded-qemu 22 | 23 | pytest-embedded-serial-esp --> pytest-embedded-serial 24 | 25 | pytest-embedded-jtag --> pytest-embedded-serial 26 | 27 | pytest-embedded-idf -->|optional, support test on espressif chips| pytest-embedded-serial-esp 28 | pytest-embedded-idf -->|optional, support test on qemu| pytest-embedded-qemu 29 | pytest-embedded-idf -->|optional, support test on wokwi| pytest-embedded-wokwi 30 | 31 | pytest-embedded-arduino -->|optional, support test on espressif chips| pytest-embedded-serial-esp 32 | pytest-embedded-arduino -->|optional, support test on qemu| pytest-embedded-qemu 33 | pytest-embedded-arduino -->|optional, support test on wokwi| pytest-embedded-wokwi 34 | ``` 35 | 36 | ## Supported Services 37 | 38 | Activate a service would enable a set of fixtures or add some extra functionalities to a few fixtures. 39 | 40 | ```{include} ../../pytest-embedded-serial/README.md 41 | ``` 42 | 43 | ```{include} ../../pytest-embedded-serial-esp/README.md 44 | ``` 45 | 46 | ```{include} ../../pytest-embedded-idf/README.md 47 | ``` 48 | 49 | ```{include} ../../pytest-embedded-jtag/README.md 50 | ``` 51 | 52 | ```{include} ../../pytest-embedded-qemu/README.md 53 | ``` 54 | 55 | ```{include} ../../pytest-embedded-arduino/README.md 56 | ``` 57 | 58 | ```{include} ../../pytest-embedded-wokwi/README.md 59 | ``` 60 | 61 | ```{include} ../../pytest-embedded-nuttx/README.md 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | from datetime import datetime 7 | 8 | # -- Project information ----------------------------------------------------- 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 10 | 11 | project = 'pytest-embedded' 12 | project_homepage = 'https://github.com/espressif/pytest-embedded' 13 | copyright = f'2023-{datetime.now().year}, Espressif Systems (Shanghai) Co., Ltd.' # noqa: A001 14 | author = 'Fu Hanxi' 15 | version = '1.x' 16 | release = '1.x' 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | 'myst_parser', 23 | 'sphinx.ext.autodoc', 24 | 'sphinx.ext.napoleon', 25 | 'sphinx.ext.intersphinx', 26 | 'sphinx_copybutton', 27 | 'sphinx_tabs.tabs', 28 | 'sphinxcontrib.mermaid', 29 | ] 30 | 31 | templates_path = ['_templates'] 32 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 33 | 34 | # -- Options for HTML output ------------------------------------------------- 35 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 36 | 37 | html_css_files = ['theme_overrides.css'] 38 | html_logo = '_static/espressif-logo.svg' 39 | html_static_path = ['_static'] 40 | html_theme = 'sphinx_rtd_theme' 41 | 42 | # -- Options for intersphinx extension --------------------------------------- 43 | 44 | intersphinx_mapping = { 45 | 'python': ('https://docs.python.org/3', None), 46 | 'pytest': ('https://docs.pytest.org/en/stable', None), 47 | 'pexpect': ('https://pexpect.readthedocs.io/en/stable', None), 48 | } 49 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ######################################### 2 | pytest-embedded |version| Documentation 3 | ######################################### 4 | 5 | .. include:: ../README.md 6 | :parser: myst_parser.sphinx_ 7 | 8 | .. toctree:: 9 | :caption: Key Concepts 10 | :glob: 11 | :hidden: 12 | 13 | concepts/* 14 | 15 | .. toctree:: 16 | :caption: Usages 17 | :glob: 18 | :hidden: 19 | 20 | usages/* 21 | 22 | .. toctree:: 23 | :caption: API Reference 24 | :hidden: 25 | :glob: 26 | 27 | apis/* 28 | 29 | .. toctree:: 30 | :caption: Other Information 31 | :hidden: 32 | 33 | CHANGELOG 34 | CONTRIBUTING 35 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -r ../requirements.txt 2 | 3 | # theme 4 | sphinx-rtd-theme 5 | 6 | # extensions 7 | myst-parser # markdown support 8 | sphinx-tabs # tabs support 9 | sphinx_copybutton # copy button 10 | sphinxcontrib-mermaid # mermaid graph support 11 | sphinxcontrib-napoleon # google style docstring support 12 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # idf related 4 | sdkconfig 5 | sdkconfig.old 6 | 7 | # qemu image 8 | flash_image.bin 9 | -------------------------------------------------------------------------------- /examples/arduino/README.md: -------------------------------------------------------------------------------- 1 | # Arduino Examples 2 | 3 | This folder contains basic examples that show how to use the Arduino service. 4 | 5 | ## Prerequisites 6 | 7 | 1. Connect to the board 8 | 2. Make sure the necessary packages are installed, at least: 9 | - `pytest_embedded` 10 | - `pytest_embedded_serial_esp` 11 | - `pytest_embedded_arduino` 12 | 3. Build an example. For this you can use `arduino-cli`. 13 | ```shell 14 | $ arduino-cli compile --build-path hello_world/build --fqbn espressif:esp32:esp32:PartitionScheme=huge_app hello_world` 15 | ``` 16 | The above command is supposed to be run from the directory `examples/arduino`, 17 | adapt the build folder appropriately when run from a different location. 18 | On success, this will create a `build` directory under the `hello_world` 19 | example. 20 | 21 | ### Run the tests 22 | 23 | ```shell 24 | $ pytest 25 | ``` 26 | 27 | This command can be run from the same location as earlier, i.e. 28 | `examples/arduino`. It can also be run from the top level directory, with: 29 | 30 | ```shell 31 | $ pytest examples/arduino -k test_hello_arduino 32 | ``` 33 | 34 | This will parse the `build` directory created earlier, flash the chip and 35 | expect the `Hello Arduino!` text to be printed. 36 | -------------------------------------------------------------------------------- /examples/arduino/hello_world/hello_world.ino: -------------------------------------------------------------------------------- 1 | void setup(){ 2 | // Open serial communications and wait for port to open: 3 | Serial.begin(115200); 4 | while (!Serial) { 5 | ; 6 | } 7 | 8 | Serial.println("Hello Arduino!"); 9 | } 10 | 11 | void loop(){ 12 | } 13 | -------------------------------------------------------------------------------- /examples/arduino/hello_world/test_hello_world.py: -------------------------------------------------------------------------------- 1 | def test_hello_arduino(dut): 2 | dut.expect('Hello Arduino!') 3 | -------------------------------------------------------------------------------- /examples/arduino/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --embedded-services esp,arduino 3 | 4 | # log related 5 | log_cli = True 6 | log_cli_level = INFO 7 | log_cli_format = %(asctime)s %(levelname)s %(message)s 8 | log_cli_date_format = %Y-%m-%d %H:%M:%S 9 | 10 | log_file = test.log 11 | log_file_level = INFO 12 | log_file_format = %(asctime)s %(levelname)s %(message)s 13 | log_file_date_format = %Y-%m-%d %H:%M:%S 14 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | # Basic DUT echo example 2 | 3 | This example shows the basics of working with a DUT. 4 | 5 | `dut` fixture in this example is provided by `pytest-embedded-serial` plugin. This fixture wraps a port implemented by `pyserial`. This is normally a serial port, however `pyserial` also supports TCP, RFC2217, loopback ports via [URL handlers](https://pyserial.readthedocs.io/en/latest/url_handlers.html). 6 | 7 | In this example, `loopback://` port is used, which means that any data sent to the DUT will be received back. The port is specified in [pytest.ini](pytest.ini). 8 | 9 | You could also try this example with a real serial port (`--port /dev/ttyUSB0`), connecting RX and TX lines. The `--port` argument specified on the command line overrides the same argument from pytest.ini. 10 | 11 | ## Prerequisites 12 | 13 | 1. Install following packages 14 | - `pytest_embedded` 15 | - `pytest_embedded_serial` 16 | 17 | ## Test Steps 18 | 19 | ```shell 20 | $ pytest 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/echo/conftest.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def open_tcp_port(): 8 | proc = subprocess.Popen('socat TCP4-LISTEN:9876,fork EXEC:cat', shell=True) 9 | yield 10 | proc.terminate() 11 | -------------------------------------------------------------------------------- /examples/echo/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --embedded-services serial -s 3 | 4 | # log related 5 | log_cli = True 6 | log_cli_level = INFO 7 | log_cli_format = %(asctime)s %(levelname)s %(message)s 8 | log_cli_date_format = %Y-%m-%d %H:%M:%S 9 | -------------------------------------------------------------------------------- /examples/echo/test_echo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize( 5 | 'port', 6 | ['socket://localhost:9876'], 7 | indirect=True, 8 | ) 9 | def test_echo_tcp(dut): 10 | dut.write(b'Hello, DUT!') 11 | dut.expect('Hello, DUT!') # will decode automatically 12 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/README.md: -------------------------------------------------------------------------------- 1 | # ESP-IDF QEMU Examples 2 | 3 | The example shows how to create idf app qemu flash image automatically and run this with qemu. 4 | 5 | The example code comes from ESP-IDF v5.0 6 | 7 | ## Prerequisites 8 | 9 | 1. Prepare QEMU program which supports xtensa, name it `qemu-system-xtensa` and add its parent directory into `$PATH` 10 | 2. Install following packages 11 | - `pytest_embedded` 12 | - `pytest_embedded_idf` 13 | - `pytest_embedded_qemu` 14 | - `esptool` (for sending commands to QEMU via socket) 15 | 3. cd into the app folder 16 | 4. run `idf.py build` under the apps you want to test 17 | 18 | ## Test Steps 19 | 20 | ```shell 21 | $ pytest 22 | ``` 23 | 24 | QEMU flash image would be created automatically if not exists 25 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/hello_world/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The following lines of boilerplate have to be in your project's 2 | # CMakeLists in this exact order for cmake to work correctly 3 | cmake_minimum_required(VERSION 3.16) 4 | 5 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 6 | project(hello_world) 7 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/hello_world/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "hello_world_main.c" 2 | INCLUDE_DIRS "") 3 | 4 | target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") 5 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/hello_world/main/hello_world_main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: CC0-1.0 5 | */ 6 | 7 | #include 8 | #include "sdkconfig.h" 9 | #include "freertos/FreeRTOS.h" 10 | #include "freertos/task.h" 11 | #include "esp_chip_info.h" 12 | #include "esp_flash.h" 13 | 14 | void app_main(void) 15 | { 16 | printf("Hello world!\n"); 17 | 18 | /* Print chip information */ 19 | esp_chip_info_t chip_info; 20 | uint32_t flash_size; 21 | esp_chip_info(&chip_info); 22 | printf("This is %s chip with %d CPU core(s), WiFi%s%s, ", 23 | CONFIG_IDF_TARGET, 24 | chip_info.cores, 25 | (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", 26 | (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); 27 | 28 | printf("silicon revision %d, ", chip_info.revision); 29 | if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) { 30 | printf("Get flash size failed"); 31 | return; 32 | } 33 | 34 | printf("%uMB %s flash\n", flash_size / (1024 * 1024), 35 | (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); 36 | 37 | printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size()); 38 | 39 | for (int i = 10; i >= 0; i--) { 40 | printf("Restarting in %d seconds...\n", i); 41 | vTaskDelay(1000 / portTICK_PERIOD_MS); 42 | } 43 | printf("Restarting now.\n"); 44 | fflush(stdout); 45 | esp_restart(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/hello_world/test_hello_world.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_embedded.log import live_print_call 3 | 4 | 5 | def test_hello_world(dut): 6 | dut.expect('Hello world!') 7 | dut.expect('Restarting') 8 | 9 | 10 | @pytest.mark.parametrize( 11 | 'qemu_extra_args', 12 | [ 13 | '"-serial tcp::5555,server,nowait -global driver=esp32.gpio,property=strap_mode,value=0x0f"', 14 | ], 15 | indirect=True, 16 | ) 17 | def test_serial_tcp(dut, redirect): 18 | with redirect(): 19 | live_print_call('esptool.py --port socket://localhost:5555 --no-stub --chip esp32 read_mac', shell=True) 20 | 21 | dut.expect('Serial port socket://localhost:5555') # expect from what esptool.py printed to sys.stdout 22 | dut.expect('MAC:') 23 | dut.expect('Hard resetting via RTS pin...') 24 | -------------------------------------------------------------------------------- /examples/esp-idf-qemu/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --embedded-services idf,qemu -s 3 | 4 | # log related 5 | log_cli = True 6 | log_cli_level = INFO 7 | log_cli_format = %(asctime)s %(levelname)s %(message)s 8 | log_cli_date_format = %Y-%m-%d %H:%M:%S 9 | 10 | log_file = test.log 11 | log_file_level = INFO 12 | log_file_format = %(asctime)s %(levelname)s %(message)s 13 | log_file_date_format = %Y-%m-%d %H:%M:%S 14 | -------------------------------------------------------------------------------- /examples/esp-idf/README.md: -------------------------------------------------------------------------------- 1 | # ESP-IDF Examples 2 | 3 | This example shows: 4 | 1. how `pytest_embedded_serial_esp` auto-detect chip target and port 5 | 2. how `pytest_embedded_idf` auto flash the app into the target chip 6 | 7 | The example code comes from ESP-IDF v5.0 8 | 9 | ## Prerequisites 10 | 11 | 1. Connect to the target chips 12 | 2. Install following packages 13 | - `pytest_embedded` 14 | - `pytest_embedded_serial_esp` 15 | - `pytest_embedded_idf` 16 | 3. cd into the app folder 17 | 4. run `idf.py build` under the apps you want to test 18 | 19 | ## Test Steps 20 | 21 | ```shell 22 | $ pytest 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/esp-idf/hello_world/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The following lines of boilerplate have to be in your project's 2 | # CMakeLists in this exact order for cmake to work correctly 3 | cmake_minimum_required(VERSION 3.16) 4 | 5 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 6 | project(hello_world) 7 | -------------------------------------------------------------------------------- /examples/esp-idf/hello_world/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "hello_world_main.c" 2 | INCLUDE_DIRS "") 3 | 4 | target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") 5 | -------------------------------------------------------------------------------- /examples/esp-idf/hello_world/main/hello_world_main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: CC0-1.0 5 | */ 6 | 7 | #include 8 | #include "sdkconfig.h" 9 | #include "freertos/FreeRTOS.h" 10 | #include "freertos/task.h" 11 | #include "esp_chip_info.h" 12 | #include "esp_flash.h" 13 | 14 | void app_main(void) 15 | { 16 | printf("Hello world!\n"); 17 | 18 | /* Print chip information */ 19 | esp_chip_info_t chip_info; 20 | uint32_t flash_size; 21 | esp_chip_info(&chip_info); 22 | printf("This is %s chip with %d CPU core(s), WiFi%s%s, ", 23 | CONFIG_IDF_TARGET, 24 | chip_info.cores, 25 | (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", 26 | (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); 27 | 28 | printf("silicon revision %d, ", chip_info.revision); 29 | if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) { 30 | printf("Get flash size failed"); 31 | return; 32 | } 33 | 34 | printf("%uMB %s flash\n", flash_size / (1024 * 1024), 35 | (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); 36 | 37 | printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size()); 38 | 39 | for (int i = 10; i >= 0; i--) { 40 | printf("Restarting in %d seconds...\n", i); 41 | vTaskDelay(1000 / portTICK_PERIOD_MS); 42 | } 43 | printf("Restarting now.\n"); 44 | fflush(stdout); 45 | esp_restart(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/esp-idf/hello_world/test_hello_world.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def test_hello_world(dut): 5 | # expect from what esptool.py printed to sys.stdout 6 | dut.expect('Hash of data verified.') 7 | 8 | # now the `dut.expect` would return a `re.Match` object if succeeded 9 | res = dut.expect(r'Hello (\w+)!') 10 | 11 | # don't forget to decode, since the serial outputs are all bytes 12 | logging.info(f'hello to {res.group(1).decode("utf8")}') 13 | 14 | # of course you can just don't care about the return value, do an assert only :) 15 | dut.expect('Restarting') 16 | -------------------------------------------------------------------------------- /examples/esp-idf/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --embedded-services esp,idf -s 3 | 4 | # log related 5 | log_cli = True 6 | log_cli_level = INFO 7 | log_cli_format = %(asctime)s %(levelname)s %(message)s 8 | log_cli_date_format = %Y-%m-%d %H:%M:%S 9 | 10 | log_file = test.log 11 | log_file_level = INFO 12 | log_file_format = %(asctime)s %(levelname)s %(message)s 13 | log_file_date_format = %Y-%m-%d %H:%M:%S 14 | -------------------------------------------------------------------------------- /examples/nuttx-esp/README.md: -------------------------------------------------------------------------------- 1 | # NuttX with Espressif Device Example 2 | 3 | The example shows run pytest-embedded using NuttX and Espressif devices. 4 | Simple uses such as serial port data exchange can be done with pytest-embedded only. 5 | Using pytest-embedded-nuttx allows for the of `esptool` along Pytest. 6 | 7 | ## Prerequisites 8 | 9 | 1. Install following packages 10 | - `pytest_embedded` 11 | - `pytest_embedded_nuttx` 12 | - `esptool` 13 | 14 | ## Test Steps 15 | 16 | ```shell 17 | $ pytest 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/nuttx-esp/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --embedded-services nuttx,esp -s --port=/dev/ttyUSB0 3 | 4 | # log related 5 | log_cli = True 6 | log_cli_level = INFO 7 | log_cli_format = %(asctime)s %(levelname)s %(message)s 8 | log_cli_date_format = %Y-%m-%d %H:%M:%S 9 | -------------------------------------------------------------------------------- /examples/nuttx-esp/test_nuttx_esp.py: -------------------------------------------------------------------------------- 1 | """Test example for Espressif devices""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def reset_to_nsh(dut, serial): 8 | """Resets the device and waits until NSH prompt is ready.""" 9 | serial.hard_reset() 10 | dut.expect('nsh>', timeout=5) 11 | 12 | 13 | def test_nuttx_esp_ps(dut): 14 | dut.write('ps') 15 | dut.expect('Idle_Task', timeout=2) 16 | -------------------------------------------------------------------------------- /foreach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | DEFAULT_PACKAGES=" \ 6 | pytest-embedded \ 7 | pytest-embedded-serial \ 8 | pytest-embedded-serial-esp \ 9 | pytest-embedded-idf \ 10 | pytest-embedded-jtag \ 11 | pytest-embedded-qemu \ 12 | pytest-embedded-arduino \ 13 | pytest-embedded-wokwi \ 14 | pytest-embedded-nuttx \ 15 | " 16 | 17 | action=${1:-"install"} 18 | res=0 19 | 20 | # one-time command 21 | pip install -U pip 22 | if [ "$action" = "build" ]; then 23 | pip install -U flit 24 | elif [ "$action" = "publish" ]; then 25 | pip install -U flit 26 | fi 27 | 28 | # for-loop each package 29 | for pkg in $DEFAULT_PACKAGES; do 30 | pushd "$pkg" 31 | if [ "$action" = "install-editable" ]; then 32 | pip install -e . 33 | elif [ "$action" = "install" ]; then 34 | pip install . 35 | elif [ "$action" = "uninstall" ]; then 36 | pip uninstall -y $pkg 37 | elif [ "$action" = "build" ]; then 38 | flit build 39 | elif [ "$action" = "publish" ]; then 40 | flit publish 41 | else 42 | echo "invalid argument. valid choices: install-editable/install/uninstall/build/publish" 43 | exit 1 44 | fi 45 | popd 46 | done 47 | 48 | exit $res 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.commitizen] 2 | name = "cz_conventional_commits" 3 | version = "1.16.1" 4 | tag_format = "v$version" 5 | version_files = [ 6 | "pytest-embedded/pytest_embedded/__init__.py", 7 | "pytest-embedded/pyproject.toml", 8 | "pytest-embedded-arduino/pytest_embedded_arduino/__init__.py", 9 | "pytest-embedded-arduino/pyproject.toml", 10 | "pytest-embedded-idf/pytest_embedded_idf/__init__.py", 11 | "pytest-embedded-idf/pyproject.toml", 12 | "pytest-embedded-jtag/pytest_embedded_jtag/__init__.py", 13 | "pytest-embedded-jtag/pyproject.toml", 14 | "pytest-embedded-qemu/pytest_embedded_qemu/__init__.py", 15 | "pytest-embedded-qemu/pyproject.toml", 16 | "pytest-embedded-serial/pytest_embedded_serial/__init__.py", 17 | "pytest-embedded-serial/pyproject.toml", 18 | "pytest-embedded-serial-esp/pytest_embedded_serial_esp/__init__.py", 19 | "pytest-embedded-serial-esp/pyproject.toml", 20 | "pytest-embedded-wokwi/pytest_embedded_wokwi/__init__.py", 21 | "pytest-embedded-wokwi/pyproject.toml", 22 | "pytest-embedded-nuttx/pytest_embedded_nuttx/__init__.py", 23 | "pytest-embedded-nuttx/pyproject.toml", 24 | ] 25 | 26 | [tool.pytest.ini_options] 27 | norecursedirs = 'examples/*' 28 | addopts = "-s" 29 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-arduino 2 | 3 | pytest embedded service for Arduino project 4 | 5 | Extra Functionalities: 6 | 7 | ```{eval-rst} 8 | .. tabs:: 9 | 10 | .. group-tab:: `pytest-embedded-serial-esp` activated 11 | 12 | - `app`: Parse Arduino's build directory and gather more information. 13 | - `serial`: Auto flash the built binary into the target board at the beginning when running test cases. 14 | 15 | .. group-tab:: `pytest-embedded-serial-esp` NOT activated 16 | 17 | - `app`: Parse Arduino's build directory and gather more information. 18 | ``` 19 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-arduino" 7 | authors = [ 8 | {name = "Abdelatif Guettouche", email = "abdelatif.guettouche@espressif.com"}, 9 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE"} 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: Pytest", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Programming Language :: Python", 29 | "Topic :: Software Development :: Testing", 30 | ] 31 | dynamic = ["version", "description"] 32 | requires-python = ">=3.7" 33 | 34 | dependencies = [ 35 | "pytest-embedded~=1.16.1", 36 | ] 37 | 38 | [project.optional-dependencies] 39 | serial = [ 40 | "pytest-embedded-serial-esp~=1.16.1" 41 | ] 42 | 43 | [project.urls] 44 | homepage = "https://github.com/espressif/pytest-embedded" 45 | repository = "https://github.com/espressif/pytest-embedded" 46 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 47 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 48 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/pytest_embedded_arduino/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with Arduino.""" 2 | 3 | import importlib 4 | 5 | from pytest_embedded.utils import lazy_load 6 | 7 | from .app import ArduinoApp 8 | 9 | __getattr__ = lazy_load( 10 | importlib.import_module(__name__), 11 | { 12 | 'ArduinoApp': ArduinoApp, 13 | }, 14 | { 15 | 'ArduinoSerial': '.serial', # requires esp 16 | }, 17 | ) 18 | 19 | __all__ = ['ArduinoApp', 'ArduinoSerial'] 20 | 21 | 22 | __version__ = '1.16.1' 23 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/pytest_embedded_arduino/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import ClassVar, Dict, List, Tuple 4 | 5 | from pytest_embedded.app import App 6 | 7 | 8 | class ArduinoApp(App): 9 | """ 10 | Arduino App class 11 | 12 | Attributes: 13 | sketch (str): Sketch name. 14 | fqbn (str): Fully Qualified Board Name. 15 | target (str) : ESPxx chip. 16 | flash_files (List[Tuple[int, str, str]]): List of (offset, file path, encrypted) of files need to be flashed in. 17 | """ 18 | 19 | #: dict of flash settings 20 | flash_settings: ClassVar[Dict[str, Dict[str, str]]] = { 21 | 'esp32': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 22 | 'esp32s2': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 23 | 'esp32c3': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 24 | 'esp32s3': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 25 | 'esp32c6': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 26 | 'esp32h2': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '48m'}, 27 | 'esp32p4': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 28 | } 29 | 30 | #: dict of binaries' offset. 31 | binary_offsets: ClassVar[Dict[str, List[int]]] = { 32 | 'esp32': [0x1000, 0x8000, 0x10000], 33 | 'esp32s2': [0x1000, 0x8000, 0x10000], 34 | 'esp32c3': [0x0, 0x8000, 0x10000], 35 | 'esp32s3': [0x0, 0x8000, 0x10000], 36 | 'esp32c6': [0x0, 0x8000, 0x10000], 37 | 'esp32h2': [0x0, 0x8000, 0x10000], 38 | 'esp32p4': [0x2000, 0x8000, 0x10000], 39 | } 40 | 41 | def __init__( 42 | self, 43 | **kwargs, 44 | ): 45 | super().__init__(**kwargs) 46 | 47 | self.sketch = os.path.basename(self.app_path) 48 | self.fqbn = self._get_fqbn(self.binary_path) 49 | self.target = self.fqbn.split(':')[2] 50 | self.flash_files = self._get_bin_files(self.binary_path, self.sketch, self.target) 51 | self.elf_file = os.path.realpath(os.path.join(self.binary_path, self.sketch + '.ino.elf')) 52 | 53 | def _get_fqbn(self, build_path) -> str: 54 | options_file = os.path.realpath(os.path.join(build_path, 'build.options.json')) 55 | with open(options_file) as f: 56 | options = json.load(f) 57 | fqbn = options['fqbn'] 58 | return fqbn 59 | 60 | def _get_bin_files(self, build_path, sketch, target) -> List[Tuple[int, str, bool]]: 61 | bootloader = os.path.realpath(os.path.join(build_path, sketch + '.ino.bootloader.bin')) 62 | partitions = os.path.realpath(os.path.join(build_path, sketch + '.ino.partitions.bin')) 63 | app = os.path.realpath(os.path.join(build_path, sketch + '.ino.bin')) 64 | files = [bootloader, partitions, app] 65 | offsets = self.binary_offsets[target] 66 | return [(offsets[i], files[i], False) for i in range(3)] 67 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/pytest_embedded_arduino/serial.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import esptool 5 | from pytest_embedded_serial_esp.serial import EspSerial 6 | 7 | from .app import ArduinoApp 8 | 9 | 10 | class ArduinoSerial(EspSerial): 11 | """ 12 | Arduino serial Dut class 13 | 14 | Auto flash the app while starting test. 15 | """ 16 | 17 | SUGGEST_FLASH_BAUDRATE = 921600 18 | 19 | def __init__( 20 | self, 21 | app: ArduinoApp, 22 | target: Optional[str] = None, 23 | **kwargs, 24 | ) -> None: 25 | self.app = app 26 | super().__init__( 27 | target=target or self.app.target, 28 | **kwargs, 29 | ) 30 | 31 | def _start(self): 32 | if self.skip_autoflash: 33 | logging.info('Skipping auto flash...') 34 | super()._start() 35 | else: 36 | self.flash() 37 | 38 | @EspSerial.use_esptool() 39 | def flash(self) -> None: 40 | """ 41 | Flash the binary files to the board. 42 | """ 43 | flash_files = [] 44 | for offset, path, encrypted in self.app.flash_files: 45 | if encrypted: 46 | continue 47 | flash_files.extend((str(offset), path)) 48 | 49 | flash_settings = [] 50 | for k, v in self.app.flash_settings[self.app.target].items(): 51 | flash_settings.append(f'--{k}') 52 | flash_settings.append(v) 53 | 54 | if self.esp_flash_force: 55 | flash_settings.append('--force') 56 | 57 | try: 58 | esptool.main( 59 | ['--chip', self.app.target, 'write_flash', *flash_files, *flash_settings], 60 | esp=self.esp, 61 | ) 62 | except Exception: 63 | raise 64 | -------------------------------------------------------------------------------- /pytest-embedded-arduino/tests/test_arduino.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_arduino_serial_flash(testdir): 5 | testdir.makepyfile(""" 6 | import pexpect 7 | import pytest 8 | 9 | def test_arduino_app(app, dut): 10 | assert len(app.flash_files) == 3 11 | assert app.target == 'esp32' 12 | assert app.fqbn == 'espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app' 13 | dut.expect('Hello Arduino!') 14 | with pytest.raises(pexpect.TIMEOUT): 15 | dut.expect('foo bar not found', timeout=1) 16 | """) 17 | 18 | result = testdir.runpytest( 19 | '-s', 20 | '--embedded-services', 21 | 'arduino,esp', 22 | '--app-path', 23 | os.path.join(testdir.tmpdir, 'hello_world_arduino'), 24 | '--build-dir', 25 | 'build', 26 | ) 27 | 28 | result.assert_outcomes(passed=1) 29 | -------------------------------------------------------------------------------- /pytest-embedded-idf/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-idf/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-idf 2 | 3 | pytest embedded service for esp-idf project 4 | 5 | Extra Functionalities: 6 | 7 | ```{eval-rst} 8 | .. tabs:: 9 | 10 | .. group-tab:: `pytest-embedded-serial-esp` activated 11 | 12 | - `app`: parse the built binary by idf rules and gather more information. 13 | - `serial`: auto flash the built binary into the target board at the beginning when running test cases. 14 | 15 | .. group-tab:: `pytest-embedded-serial-esp` NOT activated 16 | 17 | - `app`: parse the built binary by idf rules and gather more information. 18 | ``` 19 | -------------------------------------------------------------------------------- /pytest-embedded-idf/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-idf" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Framework :: Pytest", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | ] 30 | dynamic = ["version", "description"] 31 | requires-python = ">=3.7" 32 | 33 | dependencies = [ 34 | "pytest-embedded~=1.16.1", 35 | "esp-idf-panic-decoder", 36 | "esp-bool-parser>=0.1.2,<1" 37 | ] 38 | 39 | [project.optional-dependencies] 40 | serial = [ 41 | "pytest-embedded-serial-esp~=1.16.1", 42 | "esp-coredump~=1.0", 43 | ] 44 | 45 | [project.urls] 46 | homepage = "https://github.com/espressif/pytest-embedded" 47 | repository = "https://github.com/espressif/pytest-embedded" 48 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 49 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 50 | -------------------------------------------------------------------------------- /pytest-embedded-idf/pytest_embedded_idf/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with ESP-IDF.""" 2 | 3 | import importlib 4 | 5 | from pytest_embedded.utils import lazy_load 6 | 7 | from .app import IdfApp 8 | from .linux import LinuxDut, LinuxSerial 9 | from .unity_tester import CaseTester, UnittestMenuCase 10 | 11 | __getattr__ = lazy_load( 12 | importlib.import_module(__name__), 13 | { 14 | 'IdfApp': IdfApp, 15 | 'LinuxDut': LinuxDut, 16 | 'LinuxSerial': LinuxSerial, 17 | 'CaseTester': CaseTester, 18 | }, 19 | { 20 | 'IdfSerial': '.serial', # requires esp 21 | 'IdfDut': '.dut', # requires esp 22 | }, 23 | ) 24 | 25 | 26 | __all__ = [ 27 | 'CaseTester', 28 | 'IdfApp', 29 | 'IdfDut', 30 | 'IdfSerial', 31 | 'LinuxDut', 32 | 'LinuxSerial', 33 | 'UnittestMenuCase', 34 | ] 35 | 36 | __version__ = '1.16.1' 37 | -------------------------------------------------------------------------------- /pytest-embedded-idf/pytest_embedded_idf/linux.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from pytest_embedded.dut import Dut 4 | from pytest_embedded.log import DuplicateStdoutPopen 5 | 6 | from .app import IdfApp 7 | 8 | 9 | class LinuxDut(Dut): 10 | """ 11 | Dut class for Linux targets 12 | 13 | Attributes: 14 | serial (LinuxSerial): `LinuxSerial` instance 15 | """ 16 | 17 | def __init__( 18 | self, 19 | serial, 20 | **kwargs, 21 | ) -> None: 22 | self.serial = serial 23 | super().__init__(**kwargs) 24 | 25 | self._hard_reset_func = self.serial.hard_reset 26 | 27 | def write(self, data: t.AnyStr) -> None: 28 | self.serial.write(data) 29 | 30 | 31 | class LinuxSerial(DuplicateStdoutPopen): 32 | """ 33 | Linux serial Dut class 34 | """ 35 | 36 | def __init__( 37 | self, 38 | app: IdfApp, 39 | **kwargs, 40 | ) -> None: 41 | self.app = app 42 | 43 | if not hasattr(self.app, 'target'): 44 | raise ValueError(f"Idf app not parsable. Please check if it's valid: {self.app.binary_path}") 45 | 46 | if self.app.target != 'linux': 47 | raise ValueError(f'Targets do not match. App target: {self.app.target}, Cmd target: "linux".') 48 | 49 | super().__init__(cmd=[self.app.elf_file], **kwargs) 50 | 51 | def hard_reset(self) -> None: 52 | """ 53 | Perform a fake hardware reset 54 | """ 55 | self.write('\n') 56 | -------------------------------------------------------------------------------- /pytest-embedded-idf/pytest_embedded_idf/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing as t 3 | from contextvars import ContextVar 4 | 5 | import pytest 6 | from esp_bool_parser import PREVIEW_TARGETS, SUPPORTED_TARGETS 7 | from esp_bool_parser.bool_parser import parse_bool_expr 8 | 9 | supported_targets = ContextVar('supported_targets', default=SUPPORTED_TARGETS) 10 | preview_targets = ContextVar('preview_targets', default=PREVIEW_TARGETS) 11 | 12 | 13 | if sys.version_info < (3, 8): 14 | from typing_extensions import Literal 15 | else: 16 | from typing import Literal 17 | 18 | 19 | def _expand_target_values(values: t.List[t.List[t.Any]], target_index: int) -> t.List[t.List[t.Any]]: 20 | """ 21 | Expands target-specific values into individual test cases. 22 | """ 23 | expanded_values = [] 24 | for value in values: 25 | target = value[target_index] 26 | if target == 'supported_targets': 27 | expanded_values.extend([ 28 | value[:target_index] + [target] + value[target_index + 1 :] for target in supported_targets.get() 29 | ]) 30 | elif target == 'preview_targets': 31 | expanded_values.extend([ 32 | value[:target_index] + [target] + value[target_index + 1 :] for target in preview_targets.get() 33 | ]) 34 | else: 35 | expanded_values.append(value) 36 | return expanded_values 37 | 38 | 39 | def _process_pytest_value(value: t.Union[t.List[t.Any], t.Any], param_count: int) -> t.Any: 40 | """ 41 | Processes a single parameter value, converting it to pytest.param if needed. 42 | """ 43 | if not isinstance(value, (list, tuple)): 44 | return value 45 | 46 | if len(value) > param_count + 1: 47 | raise ValueError(f'Expected at most {param_count + 1} elements (params + marks), got {len(value)}') 48 | 49 | params, marks = [], [] 50 | if len(value) > param_count: 51 | mark_values = value[-1] 52 | marks.extend(mark_values if isinstance(mark_values, (tuple, list)) else (mark_values,)) 53 | 54 | params.extend(value[:param_count]) 55 | 56 | return pytest.param(*params, marks=tuple(marks)) 57 | 58 | 59 | def idf_parametrize( 60 | param_names: str, 61 | values: t.List[t.Union[t.Any, t.Tuple[t.Any, ...]]], 62 | indirect: (t.Union[bool, t.Sequence[str]]) = False, 63 | ) -> t.Callable[..., None]: 64 | """ 65 | A decorator to unify pytest.mark.parametrize usage in esp-idf. 66 | 67 | Args: 68 | param_names: A comma-separated string of parameter names that will be passed to 69 | the test function. 70 | values: A list of parameter values where each value corresponds to the parameters 71 | defined in param_names. 72 | indirect: A list of arguments names (subset of argnames) or a boolean. If True 73 | the list contains all names from the argnames. Each argvalue corresponding to an 74 | argname in this list will be passed as request.param to its respective argname 75 | fixture function so that it can perform more expensive setups during the setup 76 | phase of a test rather than at collection time. 77 | 78 | Returns: 79 | Decorated test function with parametrization applied 80 | """ 81 | param_list = [name.strip() for name in param_names.split(',')] 82 | for param in param_list: 83 | if not param: 84 | raise ValueError(f'One of the provided parameters name is empty: {param_list}') 85 | 86 | param_count = len(param_list) 87 | param_list[:] = [_p for _p in param_list if _p not in ('markers',)] 88 | target_index = param_list.index('target') if 'target' in param_list else -1 89 | normalized_values = [[value] if param_count == 1 else list(value) for value in values] 90 | param_count = len(param_list) 91 | 92 | if target_index != -1: 93 | normalized_values = _expand_target_values(normalized_values, target_index) 94 | 95 | processed_values = [_process_pytest_value(value, param_count) for value in normalized_values] 96 | 97 | def decorator(func): 98 | return pytest.mark.parametrize(','.join(param_list), processed_values, indirect=indirect)(func) 99 | 100 | return decorator 101 | 102 | 103 | ValidTargets = Literal['supported_targets', 'preview_targets', 'all'] 104 | 105 | 106 | def soc_filtered_targets(soc_statement: str, targets: ValidTargets = 'all') -> t.List[str]: 107 | """Filters targets based on a given SOC (System on Chip) statement. 108 | 109 | Args: 110 | soc_statement (str): A boolean expression used to filter targets. 111 | targets (ValidTargets, optional): Specifies which target set to filter. 112 | - "supported_targets": Filters only supported targets. 113 | - "preview_targets": Filters only preview targets. 114 | - "all": Filters both supported and preview targets. 115 | Defaults to "all". 116 | 117 | Returns: 118 | List[str]: A list of targets that satisfy the given SOC statement. 119 | """ 120 | target_list = [] 121 | target_list.extend(supported_targets.get()) if targets in ['all', 'supported_targets'] else [] 122 | target_list.extend(preview_targets.get()) if targets in ['all', 'preview_targets'] else [] 123 | 124 | stm = parse_bool_expr(soc_statement) 125 | 126 | result = [] 127 | for target in target_list: 128 | if stm.get_value(target, ''): 129 | result.append(target) 130 | return result 131 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-jtag 2 | 3 | pytest embedded service for openocd/gdb utilities 4 | 5 | Extra Functionalities: 6 | 7 | - `openocd`: enable the fixture 8 | - `gdb`: enable the fixture 9 | - `dut`: duplicate the `openocd` output and the `gdb` output to `dut.pexpect_proc`. 10 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/pytest-embedded-jtag/__init__.py -------------------------------------------------------------------------------- /pytest-embedded-jtag/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-jtag" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Framework :: Pytest", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | ] 30 | dynamic = ["version", "description"] 31 | requires-python = ">=3.7" 32 | 33 | dependencies = [ 34 | "pytest-embedded-serial~=1.16.1", 35 | ] 36 | 37 | [project.urls] 38 | homepage = "https://github.com/espressif/pytest-embedded" 39 | repository = "https://github.com/espressif/pytest-embedded" 40 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 41 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 42 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/pytest_embedded_jtag/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with JTAG.""" 2 | 3 | from ._telnetlib.telnetlib import Telnet 4 | from .gdb import Gdb 5 | from .openocd import OpenOcd 6 | 7 | __all__ = [ 8 | 'Gdb', 9 | 'OpenOcd', 10 | 'Telnet', 11 | ] 12 | 13 | __version__ = '1.16.1' 14 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/pytest_embedded_jtag/_telnetlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/pytest-embedded-jtag/pytest_embedded_jtag/_telnetlib/__init__.py -------------------------------------------------------------------------------- /pytest-embedded-jtag/pytest_embedded_jtag/gdb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import shlex 4 | import time 5 | from typing import AnyStr, Optional 6 | 7 | from pytest_embedded.log import DuplicateStdoutPopen 8 | 9 | 10 | class Gdb(DuplicateStdoutPopen): 11 | SOURCE = 'GDB' 12 | REDIRECT_CLS = None 13 | 14 | GDB_PROG_PATH = 'xtensa-esp32-elf-gdb' 15 | GDB_DEFAULT_ARGS = '--quiet' 16 | 17 | _GDB_RESPONSE_FINISHED_RE = re.compile(r'^\(gdb\)\s*$') 18 | 19 | def __init__(self, gdb_prog_path: Optional[str] = None, gdb_cli_args: Optional[str] = None, **kwargs): 20 | gdb_prog_path = gdb_prog_path or self.GDB_PROG_PATH 21 | gdb_cli_args = shlex.split(gdb_cli_args or self.GDB_DEFAULT_ARGS) 22 | 23 | self._gdb_first_write = True 24 | 25 | super().__init__(cmd=[gdb_prog_path, *gdb_cli_args], **kwargs) 26 | 27 | def write(self, s: AnyStr, non_blocking: bool = False, timeout: float = 30) -> Optional[str]: 28 | with open(self._logfile) as fr: 29 | if self._gdb_first_write: 30 | # Discard all queued responses before the first write 31 | _ = fr.readlines() 32 | self._logfile_offset = fr.tell() 33 | self._gdb_first_write = False 34 | 35 | super().write(s) 36 | 37 | if non_blocking: 38 | logging.debug('non-blocking write...') 39 | return None 40 | 41 | _buffer = '' 42 | _t_start = time.time() 43 | fr.seek(self._logfile_offset) 44 | while True: 45 | line = fr.readline() 46 | if line: 47 | _buffer += line 48 | if self._GDB_RESPONSE_FINISHED_RE.match(line): 49 | break 50 | 51 | _t_now = time.time() 52 | if (_t_now - _t_start) >= timeout: 53 | logging.debug(f'current buffer: {_buffer}') 54 | raise ValueError(f'gdb no response after {timeout} seconds') 55 | 56 | self._logfile_offset = fr.tell() 57 | 58 | logging.debug(f'{self.SOURCE} <-: {_buffer}') 59 | return _buffer 60 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/pytest_embedded_jtag/openocd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shlex 4 | import time 5 | from typing import AnyStr, Optional 6 | 7 | from pytest_embedded.log import DuplicateStdoutPopen 8 | from pytest_embedded.utils import to_bytes, to_str 9 | 10 | from ._telnetlib.telnetlib import Telnet 11 | 12 | 13 | class OpenOcd(DuplicateStdoutPopen): 14 | """ 15 | Class to communicate to OpenOCD 16 | """ 17 | 18 | SOURCE = 'OPENOCD' 19 | REDIRECT_CLS = None 20 | 21 | OPENOCD_PROG_PATH = 'openocd' 22 | OPENOCD_DEFAULT_ARGS = '-f board/esp32-wrover-kit-3.3v.cfg' 23 | 24 | TCL_BASE_PORT = 6666 25 | TELNET_BASE_PORT = 4444 26 | GDB_BASE_PORT = 3333 27 | 28 | def __init__( 29 | self, 30 | openocd_prog_path: Optional[str] = None, 31 | openocd_cli_args: Optional[str] = None, 32 | port_offset: int = 0, 33 | **kwargs, 34 | ): 35 | openocd_prog_path = openocd_prog_path or os.getenv('OPENOCD_BIN', self.OPENOCD_PROG_PATH) 36 | openocd_cli_args = shlex.split(openocd_cli_args or self.OPENOCD_DEFAULT_ARGS) 37 | 38 | openocd_scripts_path = os.getenv('OPENOCD_SCRIPTS') 39 | if openocd_scripts_path: 40 | openocd_cli_args.extend(['-s', openocd_scripts_path]) 41 | 42 | self.tcl_port = self.TCL_BASE_PORT + port_offset 43 | self.telnet_port = self.TELNET_BASE_PORT + port_offset 44 | self.gdb_port = self.GDB_BASE_PORT + port_offset 45 | 46 | openocd_cli_args.extend([ 47 | '-c', 48 | f'tcl_port {self.tcl_port}', 49 | '-c', 50 | f'telnet_port {self.telnet_port}', 51 | '-c', 52 | f'gdb_port {self.gdb_port}', 53 | ]) 54 | 55 | super().__init__(cmd=[openocd_prog_path, *openocd_cli_args], **kwargs) 56 | 57 | # open telnet port to interact with openocd 58 | for i in range(30): 59 | try: 60 | self.telnet = Telnet('127.0.0.1', self.telnet_port, 5) 61 | break 62 | except ConnectionRefusedError: 63 | time.sleep(1) 64 | else: 65 | raise ConnectionRefusedError 66 | 67 | def write(self, s: AnyStr) -> str: 68 | # read all output already sent 69 | resp = self.telnet.read_very_eager() 70 | if resp: 71 | logging.debug(f'{self.SOURCE} <-: {to_str(resp)}') 72 | 73 | logging.debug(f'{self.SOURCE} ->: {to_str(s)}') 74 | self.telnet.write(to_bytes(s, '\n')) 75 | 76 | resp = self.telnet.read_until(b'>') 77 | 78 | logging.debug(f'{self.SOURCE} <-: {to_str(resp)}') 79 | return to_str(resp) 80 | -------------------------------------------------------------------------------- /pytest-embedded-jtag/tests/test_jtag.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | jtag_connection_required = pytest.mark.skipif( 6 | os.getenv('DONT_SKIP_JTAG_TESTS', False) is False, 7 | reason='Connect the board to a JTAG adapter then use "DONT_SKIP_JTAG_TESTS" to run this test.', 8 | ) 9 | 10 | 11 | @jtag_connection_required 12 | def test_pexpect_by_jtag(testdir): 13 | testdir.makepyfile(r""" 14 | import os 15 | import time 16 | 17 | def test_pexpect_by_jtag(dut): 18 | dut.gdb.write('mon reset halt') 19 | dut.gdb.write('thb app_main') 20 | dut.gdb.write('c') 21 | dut.gdb.write('c', non_blocking=True) 22 | dut.expect('Hello world') 23 | """) 24 | 25 | result = testdir.runpytest( 26 | '-s', 27 | '--embedded-services', 28 | 'jtag,idf', 29 | '--app-path', 30 | os.path.join(testdir.tmpdir, 'hello_world_esp32'), 31 | '--port', 32 | '/dev/ttyUSB1', 33 | '--part-tool', 34 | os.path.join(testdir.tmpdir, 'gen_esp32part.py'), 35 | ) 36 | 37 | result.assert_outcomes(passed=1) 38 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-nuttx 2 | 3 | Pytest embedded service for the NuttX project, compatible with Espressif devices. 4 | 5 | Using the 'nuttx' service alongside 'serial' enables reading from and writing to the serial port, taking NuttShell into account when running programs and retrieving return codes. 6 | 7 | The `nuttx` service provides basic serial communication and testing. Adding the 'esp' service enables further capabilities for Espressif devices, including flashing and device rebooting. Alternatively, using the 'qemu' service is also supported with NuttX 8 | binaries. 9 | 10 | Additional Features: 11 | 12 | - `app`: Scans the NuttX binary directory to locate firmware and bootloader files. 13 | - `serial`: Parses binary information and flashes the board. Requires the 'esp' service. 14 | - `dut`: Sends commands to the device through the serial port. Requires the 'serial' service, 'esp' service for Espressif devices or 'qemu' service for emulation. 15 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-nuttx" 7 | authors = [ 8 | {name = "Filipe Cavalcanti", email = "filipe.cavalcanti@espressif.com"}, 9 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE"} 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: Pytest", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Programming Language :: Python", 29 | "Topic :: Software Development :: Testing", 30 | ] 31 | dynamic = ["version", "description"] 32 | requires-python = ">=3.7" 33 | 34 | dependencies = [ 35 | "pytest-embedded-serial~=1.16.1", 36 | ] 37 | 38 | [project.optional-dependencies] 39 | esp = [ 40 | "pytest-embedded-serial-esp~=1.16.1", 41 | ] 42 | qemu = [ 43 | "pytest-embedded-qemu~=1.16.1", 44 | ] 45 | 46 | [project.urls] 47 | homepage = "https://github.com/espressif/pytest-embedded" 48 | repository = "https://github.com/espressif/pytest-embedded" 49 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 50 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 51 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pytest_embedded_nuttx/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with NuttX.""" 2 | 3 | import importlib 4 | 5 | from pytest_embedded.utils import lazy_load 6 | 7 | from .app import NuttxApp 8 | from .dut import NuttxDut, NuttxSerialDut 9 | 10 | __getattr__ = lazy_load( 11 | importlib.import_module(__name__), 12 | { 13 | 'NuttxApp': NuttxApp, 14 | 'NuttxDut': NuttxDut, 15 | 'NuttxSerialDut': NuttxSerialDut, 16 | }, 17 | { 18 | 'NuttxSerial': '.serial', # requires 'esp' service 19 | 'NuttxEspDut': '.serial', # requires 'esp' service 20 | 'NuttxQemuDut': '.qemu', # requires 'qemu' service 21 | }, 22 | ) 23 | 24 | __all__ = ['NuttxApp', 'NuttxDut', 'NuttxEspDut', 'NuttxQemuDut', 'NuttxSerial', 'NuttxSerialDut'] 25 | 26 | __version__ = '1.16.1' 27 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pytest_embedded_nuttx/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing as t 3 | from pathlib import Path 4 | 5 | from pytest_embedded.app import App 6 | 7 | 8 | class NuttxApp(App): 9 | """ 10 | NuttX App class for Espressif devices. 11 | Evaluates binary files (firmware and bootloader) and extract information 12 | required for flashing. 13 | 14 | Attributes: 15 | file_extension (str): app binary file extension. 16 | """ 17 | 18 | def __init__( 19 | self, 20 | file_extension='.bin', 21 | **kwargs, 22 | ): 23 | super().__init__(**kwargs) 24 | 25 | self.file_extension = file_extension 26 | self.app_file, self.bootloader_file, self.merge_file = self._get_bin_files() 27 | 28 | def _get_bin_files(self) -> t.Tuple[t.Optional[Path], t.Optional[Path], t.Optional[Path]]: 29 | """ 30 | Get path to binary files available in the app_path. 31 | If either the application image or bootloader is not found, 32 | None is returned. 33 | 34 | Returns: 35 | tuple: path to application binary file and bootloader file. 36 | """ 37 | search_path = Path(self.app_path) 38 | search_pattern = '*' + self.file_extension 39 | bin_files = list(search_path.rglob(search_pattern)) 40 | app_file, bootloader_file, merge_file = None, None, None 41 | 42 | logging.info('Searching %s', str(search_path)) 43 | if not bin_files: 44 | logging.warning('No binary files found with pattern: %s', search_pattern) 45 | 46 | for file_path in bin_files: 47 | file_name = str(file_path.stem) 48 | if 'nuttx' in file_name: 49 | if 'merged' in file_name: 50 | merge_file = file_path 51 | logging.info('Merge file: %s', merge_file.as_posix()) 52 | else: 53 | app_file = file_path 54 | logging.info('App file: %s', app_file.as_posix()) 55 | if 'mcuboot-' in file_name: 56 | bootloader_file = file_path 57 | logging.info('Bootloader file: %s', bootloader_file.as_posix()) 58 | 59 | if not app_file: 60 | logging.error('App file not found: %s', app_file) 61 | print(bin_files) 62 | 63 | return app_file, bootloader_file, merge_file 64 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pytest_embedded_nuttx/dut.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from time import sleep 4 | from typing import AnyStr 5 | 6 | import pexpect 7 | from pytest_embedded.dut import Dut 8 | from pytest_embedded_serial import SerialDut 9 | 10 | 11 | class NuttxDut(Dut): 12 | """ 13 | Generic DUT class for use with NuttX RTOS. 14 | """ 15 | 16 | PROMPT_NSH = 'nsh>' 17 | PROMPT_TIMEOUT_S = 30 18 | 19 | def write(self, data: str) -> None: 20 | """ 21 | Write to NuttShell and sleep for a few hundred milliseconds to 22 | ensure there is time for Nuttshell prompt appear again. 23 | 24 | Args: 25 | data (str): data to be passed on to Nuttshell. 26 | 27 | Returns: 28 | None. 29 | """ 30 | super().write('') 31 | self.expect(self.PROMPT_NSH, timeout=self.PROMPT_TIMEOUT_S) 32 | super().write(data) 33 | sleep(0.25) 34 | 35 | def return_code(self, timeout: int = PROMPT_TIMEOUT_S) -> int: 36 | """ 37 | Matches the 'echo $?' response and extracts the integer value 38 | corresponding to the last program return code. 39 | 40 | The first regex option on expect is for serial interface, 41 | while the second will match QEMU. 42 | 43 | Returns: 44 | int: return code. 45 | """ 46 | self.write('echo $?') 47 | echo_match = self.expect([r'echo \$\?\r\n(\d+)', r'echo \$\?\n*(\d+)\n'], timeout=timeout) 48 | ret_code = re.findall(r'\d+', echo_match.group().decode()) 49 | 50 | if not ret_code: 51 | logging.error('Failed to retrieve return code') 52 | 53 | return int(ret_code[0]) 54 | 55 | def write_and_return(self, data: str, timeout: int = 2) -> AnyStr: 56 | """ 57 | Writes to Nuttshell and returns all available serial data until 58 | the timeout. 59 | This is useful when parsing and reusing the data is required, and 60 | pexect is not enough. 61 | 62 | Args: 63 | data (str): data to be passed on to Nuttshell. 64 | timeout (int): how long to wait for an answer in seconds. 65 | 66 | Returns: 67 | AnyStr 68 | """ 69 | self.write(data) 70 | ans = self.expect(pexpect.TIMEOUT, timeout=timeout) 71 | return ans.rstrip().decode() 72 | 73 | def reset_to_nsh(self, ready_prompt: str = PROMPT_NSH) -> None: 74 | """ 75 | Resets the board and waits until the Nuttshell prompt appears. 76 | Defaults to 'nsh>'. 77 | 78 | Args: 79 | ready_prompt (str): string on prompt that signals completion. 80 | 81 | Returns: 82 | None 83 | """ 84 | if self.reset: 85 | logging.info('Resetting board') 86 | self.reset() 87 | else: 88 | logging.error('Resetting method not available') 89 | self.expect(ready_prompt, timeout=self.PROMPT_TIMEOUT_S) 90 | 91 | 92 | class NuttxSerialDut(SerialDut, NuttxDut): 93 | """ 94 | DUT class for serial ports connected to generic boards running NuttX 95 | with NuttX RTOS. 96 | """ 97 | 98 | def __init__( 99 | self, 100 | **kwargs, 101 | ) -> None: 102 | super().__init__(**kwargs) 103 | 104 | def reset(self) -> None: 105 | """Reset the DUT by toggling the DTR line.""" 106 | self.serial.proc.dtr = False 107 | sleep(0.2) 108 | self.serial.proc.dtr = True 109 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pytest_embedded_nuttx/qemu.py: -------------------------------------------------------------------------------- 1 | from pytest_embedded_qemu import Qemu, QemuDut 2 | 3 | from .dut import NuttxDut 4 | 5 | 6 | class NuttxQemuDut(QemuDut, NuttxDut): 7 | """ 8 | DUT class for QEMU usage of the NuttX RTOS. 9 | """ 10 | 11 | def __init__( 12 | self, 13 | qemu: Qemu, 14 | **kwargs, 15 | ) -> None: 16 | self.qemu = qemu 17 | 18 | super().__init__(qemu=qemu, **kwargs) 19 | 20 | def reset(self) -> None: 21 | """Hard reset the DUT.""" 22 | self.hard_reset() 23 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/pytest_embedded_nuttx/serial.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar, Dict 2 | 3 | import esptool 4 | from esptool.cmds import FLASH_MODES, LoadFirmwareImage 5 | from pytest_embedded_serial_esp.serial import EspSerial 6 | 7 | from .app import NuttxApp 8 | from .dut import NuttxSerialDut 9 | 10 | 11 | class NuttxSerial(EspSerial): 12 | """ 13 | NuttX serial DUT class. 14 | """ 15 | 16 | # Default offset for the primary MCUBoot slot across 17 | # all Espressif devices on NuttX 18 | MCUBOOT_PRIMARY_SLOT_OFFSET = 0x10000 19 | SERIAL_BAUDRATE = 115200 20 | 21 | binary_offsets: ClassVar[Dict[str, int]] = { 22 | 'esp32': 0x1000, 23 | 'esp32s2': 0x1000, 24 | 'esp32c3': 0x0, 25 | 'esp32s3': 0x0, 26 | 'esp32c6': 0x0, 27 | 'esp32h2': 0x0, 28 | 'esp32p4': 0x2000, 29 | } 30 | 31 | def __init__( 32 | self, 33 | app: NuttxApp, 34 | **kwargs, 35 | ) -> None: 36 | self.app = app 37 | super().__init__(**kwargs) 38 | self.flash_size = None 39 | self.flash_freq = None 40 | self.flash_mode = None 41 | self._get_binary_target_info() 42 | 43 | def _get_binary_target_info(self): 44 | """Binary target should be in the format nuttx.merged.bin, where 45 | the 'merged.bin' extension can be modified by the file_extension 46 | argument. 47 | 48 | Important note regarding MCUBoot: 49 | If enabled, the magic number will be on the MCUBoot binary. In this 50 | case, image_info should run on the mcuboot binary, not the NuttX one. 51 | """ 52 | 53 | def get_key_from_value(dictionary, val): 54 | """Get key from value in dictionary""" 55 | for key, value in dictionary.items(): 56 | if value == val: 57 | return key 58 | return None 59 | 60 | binary_path = self.app.app_file 61 | if self.app.bootloader_file: 62 | binary_path = self.app.bootloader_file 63 | 64 | # Load app image and retrieve flash information 65 | image = LoadFirmwareImage(self.target, binary_path.as_posix()) 66 | 67 | # Flash Size 68 | flash_s_bits = image.flash_size_freq & 0xF0 69 | self.flash_size = get_key_from_value(image.ROM_LOADER.FLASH_SIZES, flash_s_bits) 70 | 71 | # Flash Frequency 72 | flash_fr_bits = image.flash_size_freq & 0x0F # low four bits 73 | self.flash_freq = get_key_from_value(image.ROM_LOADER.FLASH_FREQUENCY, flash_fr_bits) 74 | 75 | # Flash Mode 76 | self.flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode) 77 | 78 | # From esp-idf/components/esptool_py: 79 | # We use esptool.py to flash bootloader in dio mode for QIO/QOUT, bootloader then 80 | # upgrades itself to quad mode during initialization. 81 | if self.flash_mode in ('qio', 'qout'): 82 | self.flash_mode = 'dio' 83 | 84 | @EspSerial.use_esptool() 85 | def flash(self) -> None: 86 | """Flash the binary files to the board.""" 87 | 88 | flash_files = [] 89 | if self.app.bootloader_file: 90 | flash_files.extend((str(self.binary_offsets[self.target]), self.app.bootloader_file.as_posix())) 91 | flash_files.extend((str(self.MCUBOOT_PRIMARY_SLOT_OFFSET), self.app.app_file.as_posix())) 92 | else: 93 | flash_files.extend((str(self.binary_offsets[self.target]), self.app.app_file.as_posix())) 94 | 95 | flash_settings = [ 96 | '--flash_mode', 97 | self.flash_mode, 98 | '--flash_size', 99 | self.flash_size, 100 | '--flash_freq', 101 | self.flash_freq, 102 | ] 103 | 104 | esptool.main( 105 | [ 106 | '--chip', 107 | self.target, 108 | '--port', 109 | self.port, 110 | '--baud', 111 | str(self.esptool_baud), 112 | 'write_flash', 113 | *flash_files, 114 | *flash_settings, 115 | ], 116 | esp=self.esp, 117 | ) 118 | 119 | 120 | class NuttxEspDut(NuttxSerialDut): 121 | """ 122 | DUT class for serial ports connected to Espressif boards which are 123 | flashed with NuttX RTOS. 124 | """ 125 | 126 | def __init__( 127 | self, 128 | app: NuttxApp, 129 | **kwargs, 130 | ) -> None: 131 | super().__init__(app=app, **kwargs) 132 | 133 | def reset(self) -> None: 134 | """Resets the board.""" 135 | self.serial: NuttxSerial 136 | self.serial.hard_reset() 137 | -------------------------------------------------------------------------------- /pytest-embedded-nuttx/tests/test_nuttx.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import pytest 5 | 6 | 7 | def test_nuttx_app(testdir): 8 | testdir.makepyfile(""" 9 | import pytest 10 | 11 | def test_nuttx_app(dut, app, target): 12 | assert 'esp32' == target 13 | assert '40m' == dut.serial.flash_freq 14 | assert '4MB' == dut.serial.flash_size 15 | assert 'dio' == dut.serial.flash_mode 16 | 17 | def test_app_flash(serial, dut): 18 | serial.erase_flash() 19 | serial.flash() 20 | dut.reset_to_nsh() 21 | 22 | def test_hello(dut): 23 | dut.reset_to_nsh() 24 | dut.write("ls /dev") 25 | dut.expect("console") 26 | """) 27 | 28 | result = testdir.runpytest( 29 | '-s', 30 | '--embedded-services', 31 | 'nuttx,esp', 32 | '--target', 33 | 'esp32', 34 | '--app-path', 35 | os.path.join(testdir.tmpdir, 'hello_world_nuttx'), 36 | ) 37 | 38 | result.assert_outcomes(passed=3) 39 | 40 | 41 | def test_nuttx_app_mcuboot(testdir): 42 | testdir.makepyfile(""" 43 | import pytest 44 | 45 | def test_nuttx_app_mcuboot(dut, app, target): 46 | assert 'esp32' == target 47 | assert '40m' == dut.serial.flash_freq 48 | assert '4MB' == dut.serial.flash_size 49 | assert 'dio' == dut.serial.flash_mode 50 | assert None != app.bootloader_file 51 | 52 | def test_nuttx_app_mcuboot_flash(serial, dut): 53 | serial.erase_flash() 54 | serial.flash() 55 | dut.reset_to_nsh() 56 | 57 | def test_hello_mcuboot(dut): 58 | dut.reset_to_nsh() 59 | dut.write("ls /dev") 60 | dut.expect("console") 61 | """) 62 | 63 | result = testdir.runpytest( 64 | '-s', 65 | '--embedded-services', 66 | 'nuttx,esp', 67 | '--target', 68 | 'esp32', 69 | '--app-path', 70 | os.path.join(testdir.tmpdir, 'hello_world_nuttx_mcuboot'), 71 | ) 72 | 73 | result.assert_outcomes(passed=3) 74 | 75 | 76 | qemu_bin_required = pytest.mark.skipif( 77 | shutil.which('qemu-system-xtensa') is None, 78 | reason='Please make sure that `qemu-system-xtensa` is in your PATH env var. Build QEMU for ESP32 locally and then ' 79 | 'run `pytest` again', 80 | ) 81 | 82 | 83 | @qemu_bin_required 84 | def test_nuttx_app_qemu(testdir): 85 | testdir.makepyfile(""" 86 | import pytest 87 | from time import sleep 88 | 89 | def test_nuttx_app_qemu(dut): 90 | # Wait boot sequence 91 | sleep(1) 92 | dut.write("ls /dev") 93 | dut.expect("console") 94 | """) 95 | 96 | efuse_path = os.path.join(testdir.tmpdir, 'hello_world_nuttx_qemu', 'qemu_efuse.bin') 97 | qemu_extra = f'-drive file={efuse_path},if=none,format=raw,id=efuse ' 98 | qemu_extra += '-global driver=nvram.esp32.efuse,property=drive,value=efuse' 99 | 100 | print(qemu_extra) 101 | result = testdir.runpytest( 102 | '-s', 103 | '--embedded-services', 104 | 'nuttx,qemu', 105 | '--target', 106 | 'esp32', 107 | '--app-path', 108 | os.path.join(testdir.tmpdir, 'hello_world_nuttx_qemu'), 109 | '--qemu-image-path', 110 | os.path.join(testdir.tmpdir, 'hello_world_nuttx_qemu', 'nuttx.merged.bin'), 111 | '--qemu-extra-args', 112 | qemu_extra, 113 | '--qemu-prog-path', 114 | 'qemu-system-xtensa', 115 | ) 116 | 117 | result.assert_outcomes(passed=1) 118 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-qemu 2 | 3 | pytest embedded service for running tests on QEMU instead of the real target. 4 | 5 | Extra Functionalities: 6 | 7 | ```{eval-rst} 8 | .. tabs:: 9 | 10 | .. group-tab:: `pytest-embedded-idf` activated 11 | 12 | - `app`: create the qemu bootable image automatically by the built binaries. 13 | - `qemu`: enable the fixture 14 | - `dut`: duplicate the `qemu` output to `pexpect_proc`. 15 | 16 | .. group-tab:: `pytest-embedded-idf` NOT activated 17 | 18 | - `qemu`: enable the fixture 19 | - `dut`: duplicate the `qemu` output to `pexpect_proc`. 20 | ``` 21 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-qemu" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Framework :: Pytest", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | ] 30 | dynamic = ["version", "description"] 31 | requires-python = ">=3.7" 32 | 33 | dependencies = [ 34 | "pytest-embedded~=1.16.1", 35 | "qemu.qmp==0.0.3" 36 | ] 37 | 38 | [project.optional-dependencies] 39 | idf = [ 40 | "pytest-embedded-idf~=1.16.1", 41 | ] 42 | 43 | [project.urls] 44 | homepage = "https://github.com/espressif/pytest-embedded" 45 | repository = "https://github.com/espressif/pytest-embedded" 46 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 47 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 48 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/pytest_embedded_qemu/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with QEMU.""" 2 | 3 | import importlib 4 | 5 | from pytest_embedded.utils import lazy_load 6 | 7 | DEFAULT_IMAGE_FN = 'flash_image.bin' 8 | ENCRYPTED_IMAGE_FN = f'encrypted_{DEFAULT_IMAGE_FN}' 9 | 10 | from .dut import QemuDut # noqa 11 | from .qemu import Qemu # noqa 12 | 13 | 14 | __getattr__ = lazy_load( 15 | importlib.import_module(__name__), 16 | { 17 | 'Qemu': Qemu, 18 | 'QemuDut': QemuDut, 19 | }, 20 | { 21 | 'QemuApp': '.app', # requires idf 22 | }, 23 | ) 24 | 25 | 26 | __all__ = [ 27 | 'DEFAULT_IMAGE_FN', 28 | 'ENCRYPTED_IMAGE_FN', 29 | 'Qemu', 30 | 'QemuApp', 31 | 'QemuDut', 32 | ] 33 | 34 | __version__ = '1.16.1' 35 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/pytest_embedded_qemu/dut.py: -------------------------------------------------------------------------------- 1 | from typing import AnyStr 2 | 3 | from pytest_embedded.dut import Dut 4 | 5 | from .qemu import Qemu 6 | 7 | 8 | class QemuDut(Dut): 9 | """ 10 | QEMU dut class 11 | """ 12 | 13 | def __init__( 14 | self, 15 | qemu: Qemu, 16 | **kwargs, 17 | ) -> None: 18 | self.qemu = qemu 19 | 20 | super().__init__(**kwargs) 21 | 22 | self._hard_reset_func = self.qemu._hard_reset 23 | 24 | def write(self, s: AnyStr) -> None: 25 | self.qemu.write(s) 26 | 27 | def hard_reset(self): 28 | self._hard_reset_func() 29 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/pytest_embedded_qemu/qemu.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import shlex 5 | import socket 6 | import typing as t 7 | 8 | from pytest_embedded.log import DuplicateStdoutPopen 9 | from qemu.qmp import QMPClient 10 | 11 | from . import DEFAULT_IMAGE_FN 12 | 13 | if t.TYPE_CHECKING: 14 | from .app import QemuApp 15 | 16 | 17 | class Qemu(DuplicateStdoutPopen): 18 | """ 19 | QEMU class 20 | """ 21 | 22 | SOURCE = 'QEMU' 23 | 24 | QEMU_PROG_PATH = 'qemu-system-xtensa' 25 | 26 | QEMU_DEFAULT_ARGS = '-nographic -machine esp32' 27 | QEMU_DEFAULT_FMT = '-nographic -machine {}' 28 | 29 | QEMU_STRAP_MODE_FMT = '-global driver=esp32.gpio,property=strap_mode,value={}' 30 | QEMU_SERIAL_TCP_FMT = '-serial tcp::{},server,nowait' 31 | 32 | QEMU_DEFAULT_QMP_FMT = '-qmp tcp:127.0.0.1:{},server,wait=off' 33 | 34 | def __init__( 35 | self, 36 | qemu_image_path: t.Optional[str] = None, 37 | qemu_prog_path: t.Optional[str] = None, 38 | qemu_cli_args: t.Optional[str] = None, 39 | qemu_extra_args: t.Optional[str] = None, 40 | app: t.Optional['QemuApp'] = None, 41 | **kwargs, 42 | ): 43 | """ 44 | Args: 45 | qemu_image_path: QEMU image path 46 | qemu_prog_path: QEMU program path 47 | qemu_cli_args: QEMU CLI arguments 48 | qemu_extra_args: QEMU CLI extra arguments, will be appended to `qemu_cli_args` 49 | """ 50 | self.app = app 51 | 52 | image_path = qemu_image_path or DEFAULT_IMAGE_FN 53 | if not os.path.exists(image_path): 54 | raise ValueError(f"QEMU image path doesn't exist: {image_path}") 55 | 56 | qemu_prog_path = qemu_prog_path or self.qemu_prog_name 57 | 58 | if qemu_cli_args: 59 | qemu_cli_args = qemu_cli_args.strip('"').strip("'") 60 | qemu_cli_args = shlex.split(qemu_cli_args or self.qemu_default_args) 61 | qemu_extra_args = shlex.split(qemu_extra_args or '') 62 | 63 | self.qmp_addr = None 64 | self.qmp_port = None 65 | 66 | dut_index = int(kwargs.pop('dut_index', 0)) 67 | for i, v in enumerate(qemu_cli_args): 68 | if v == '-qmp': 69 | d = qemu_cli_args[i + 1] 70 | if not d.startswith('tcp'): 71 | raise ValueError('Please use TCP for qmp, example: -qmp tcp:localhost:4488,server,wait=off') 72 | cmd = d.split(',') 73 | _, self.qmp_addr, self.qmp_port = cmd[0].split(':') 74 | self.qmp_port = int(self.qmp_port) + dut_index 75 | cmd[0] = f'tcp:{self.qmp_addr}:{self.qmp_port}' 76 | qemu_cli_args[i + 1] = ','.join(cmd) 77 | break 78 | else: 79 | self.qmp_addr = '127.0.0.1' 80 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 81 | s.bind((self.qmp_addr, 0)) 82 | _, self.qmp_port = s.getsockname() 83 | qemu_cli_args += shlex.split(self.QEMU_DEFAULT_QMP_FMT.format(self.qmp_port)) 84 | 85 | super().__init__( 86 | cmd=[qemu_prog_path, *qemu_cli_args, *qemu_extra_args, '-drive', f'file={image_path},if=mtd,format=raw'], 87 | **kwargs, 88 | ) 89 | 90 | @property 91 | def qemu_prog_name(self): 92 | if self.app: 93 | return self.app.qemu_prog_path 94 | 95 | logging.warning('App not set, use default qemu program name "%s"', self.QEMU_DEFAULT_PROG_PATH) 96 | return self.QEMU_PROG_PATH 97 | 98 | @property 99 | def qemu_default_args(self): 100 | if self.app: 101 | try: 102 | return self.QEMU_DEFAULT_FMT.format(self.app.target) 103 | except AttributeError: 104 | pass 105 | 106 | return self.QEMU_DEFAULT_ARGS 107 | 108 | def qmp_execute_cmd(self, execute, arguments=None): 109 | response = None 110 | 111 | async def h_r(): 112 | nonlocal response 113 | 114 | qmp = QMPClient() 115 | try: 116 | await qmp.connect((str(self.qmp_addr), int(self.qmp_port))) 117 | response = await qmp.execute(execute, arguments=arguments) 118 | finally: 119 | await qmp.disconnect() 120 | 121 | asyncio.run(h_r()) 122 | return response 123 | 124 | def _hard_reset(self): 125 | self.qmp_execute_cmd('system_reset') 126 | 127 | def take_screenshot(self, image_path): 128 | self.qmp_execute_cmd('screendump', arguments={'filename': image_path}) 129 | -------------------------------------------------------------------------------- /pytest-embedded-qemu/tests/test_qemu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import xml.etree.ElementTree as ET 4 | 5 | import pytest 6 | 7 | qemu_bin_required = pytest.mark.skipif( 8 | shutil.which('qemu-system-xtensa') is None, 9 | reason='Please make sure that `qemu-system-xtensa` is in your PATH env var. Build QEMU for ESP32 locally and then ' 10 | 'run `pytest` again', 11 | ) 12 | 13 | 14 | @qemu_bin_required 15 | def test_pexpect_by_qemu_xtensa(testdir): 16 | testdir.makepyfile(""" 17 | import pexpect 18 | import pytest 19 | 20 | def test_pexpect_by_qemu(dut): 21 | dut.expect('Hello world!') 22 | dut.expect('Restarting') 23 | with pytest.raises(pexpect.TIMEOUT): 24 | dut.expect('foo bar not found', timeout=1) 25 | """) 26 | 27 | result = testdir.runpytest( 28 | '-s', 29 | '--embedded-services', 30 | 'idf,qemu', 31 | '--app-path', 32 | os.path.join(testdir.tmpdir, 'hello_world_esp32'), 33 | ) 34 | 35 | result.assert_outcomes(passed=1) 36 | 37 | 38 | @qemu_bin_required 39 | def test_pexpect_make_restart_by_qemu_xtensa(testdir): 40 | testdir.makepyfile(""" 41 | import pexpect 42 | import pytest 43 | 44 | def test_pexpect_by_qemu(dut): 45 | dut.expect('Hello world!') 46 | dut.hard_reset() 47 | dut.expect('cpu_start') 48 | dut.expect('Hello world!') 49 | dut.hard_reset() 50 | dut.expect('cpu_start') 51 | dut.expect('Hello world!') 52 | """) 53 | 54 | result = testdir.runpytest( 55 | '-s', 56 | '--embedded-services', 57 | 'idf,qemu', 58 | '--app-path', 59 | os.path.join(testdir.tmpdir, 'hello_world_esp32'), 60 | '--qemu-cli-args="-machine esp32 -nographic"', 61 | ) 62 | 63 | result.assert_outcomes(passed=1) 64 | 65 | 66 | @qemu_bin_required 67 | def test_pexpect_by_qemu_riscv(testdir): 68 | testdir.makepyfile(""" 69 | import pexpect 70 | import pytest 71 | 72 | def test_pexpect_by_qemu(dut): 73 | dut.expect('Hello world!') 74 | dut.expect('Restarting') 75 | with pytest.raises(pexpect.TIMEOUT): 76 | dut.expect('foo bar not found', timeout=1) 77 | """) 78 | 79 | result = testdir.runpytest( 80 | '-s', 81 | '--embedded-services', 82 | 'idf,qemu', 83 | '--app-path', 84 | os.path.join(testdir.tmpdir, 'hello_world_esp32c3'), 85 | ) 86 | 87 | result.assert_outcomes(passed=1) 88 | 89 | 90 | @qemu_bin_required 91 | def test_multi_count_qemu(testdir): 92 | testdir.makepyfile(""" 93 | import pexpect 94 | import pytest 95 | 96 | def test_pexpect_by_qemu(dut): 97 | dut[0].expect('Hello world!') 98 | dut[1].expect('Restarting') 99 | """) 100 | 101 | result = testdir.runpytest( 102 | '-s', 103 | '--count', 104 | 2, 105 | '--embedded-services', 106 | 'idf,qemu|qemu', 107 | '--app-path', 108 | f'{os.path.join(testdir.tmpdir, "hello_world_esp32")}|', 109 | '--qemu-image-path', 110 | f'|{os.path.join(testdir.tmpdir, "esp32_qemu.bin")}', 111 | ) 112 | 113 | result.assert_outcomes(passed=1) 114 | 115 | 116 | @qemu_bin_required 117 | def test_pre_flash_enc_qemu(testdir): 118 | testdir.makepyfile(""" 119 | import pexpect 120 | import pytest 121 | 122 | def test_pexpect_by_qemu(dut): 123 | dut.expect('Hello world!', timeout=120) 124 | dut.expect('Restarting') 125 | with pytest.raises(pexpect.TIMEOUT): 126 | dut.expect('foo bar not found', timeout=1) 127 | """) 128 | 129 | app_path = os.path.join(testdir.tmpdir, 'hello_world_esp32_flash_enc') 130 | keyfile_path = os.path.join(app_path, 'pre_encryption_key.bin') 131 | efuses_path = os.path.join(app_path, 'pre_encryption_efuses.bin') 132 | 133 | result = testdir.runpytest( 134 | '-s', 135 | '--embedded-services', 136 | 'idf,qemu', 137 | '--app-path', 138 | app_path, 139 | '--qemu-extra-args', 140 | f'-drive file={efuses_path},if=none,format=raw,id=efuse' 141 | ' -global driver=nvram.esp32.efuse,property=drive,value=efuse', 142 | '--encrypt', 143 | 'true', 144 | '--keyfile', 145 | keyfile_path, 146 | ) 147 | 148 | result.assert_outcomes(passed=1) 149 | 150 | 151 | @qemu_bin_required 152 | def test_qemu_use_idf_mixin_methods(testdir): 153 | testdir.makepyfile(""" 154 | import pexpect 155 | import pytest 156 | 157 | def test_qemu_use_idf_mixin_methods(dut): 158 | dut.run_all_single_board_cases() 159 | """) 160 | 161 | result = testdir.runpytest( 162 | '-s', 163 | '--embedded-services', 164 | 'idf,qemu', 165 | '--app-path', 166 | f'{os.path.join(testdir.tmpdir, "unit_test_app_esp32")}', 167 | '--junitxml', 168 | 'report.xml', 169 | ) 170 | 171 | result.assert_outcomes(failed=1) 172 | 173 | junit_report = ET.parse('report.xml').getroot()[0] 174 | 175 | assert junit_report.attrib['errors'] == '0' 176 | assert junit_report.attrib['failures'] == '1' 177 | assert junit_report.attrib['skipped'] == '0' 178 | assert junit_report.attrib['tests'] == '2' 179 | -------------------------------------------------------------------------------- /pytest-embedded-serial-esp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-serial-esp/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-serial-esp 2 | 3 | pytest embedded service for testing espressif boards via serial ports 4 | 5 | Extra Functionalities: 6 | 7 | - `serial`: detect and confirm target and port by `esptool` automatically. 8 | -------------------------------------------------------------------------------- /pytest-embedded-serial-esp/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-serial-esp" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Framework :: Pytest", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | ] 30 | dynamic = ["version", "description"] 31 | requires-python = ">=3.7" 32 | 33 | dependencies = [ 34 | "pytest-embedded-serial~=1.16.1", 35 | "esptool~=4.5", 36 | ] 37 | 38 | [project.urls] 39 | homepage = "https://github.com/espressif/pytest-embedded" 40 | repository = "https://github.com/espressif/pytest-embedded" 41 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 42 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 43 | -------------------------------------------------------------------------------- /pytest-embedded-serial-esp/pytest_embedded_serial_esp/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with Espressif target boards.""" 2 | 3 | from .serial import EspSerial 4 | 5 | __all__ = [ 6 | 'EspSerial', 7 | ] 8 | 9 | __version__ = '1.16.1' 10 | -------------------------------------------------------------------------------- /pytest-embedded-serial-esp/tests/test_esp.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | 6 | def test_detect_port(testdir): 7 | testdir.makepyfile( 8 | """ 9 | def test_detect_port(dut): 10 | assert dut[0].serial.target == 'esp32s2' 11 | assert dut[1].serial.target == 'esp32' 12 | """ 13 | ) 14 | 15 | result = testdir.runpytest( 16 | '-s', 17 | '--count', 18 | 2, 19 | '--embedded-services', 20 | 'esp', 21 | '--target', 22 | 'esp32s2|esp32', 23 | ) 24 | 25 | result.assert_outcomes(passed=1) 26 | 27 | 28 | def test_detect_port_with_cache(testdir, caplog, first_index_of_messages): 29 | testdir.makepyfile( 30 | """ 31 | def test_detect_port(dut): 32 | assert dut[0].serial.target == 'esp32s2' 33 | assert dut[1].serial.target == 'esp32' 34 | 35 | def test_detect_port_again(dut): 36 | assert dut[0].serial.target == 'esp32s2' 37 | assert dut[1].serial.target == 'esp32' 38 | """ 39 | ) 40 | 41 | result = testdir.runpytest( 42 | '-s', 43 | '--count', 44 | 2, 45 | '--embedded-services', 46 | 'esp', 47 | '--target', 48 | 'esp32s2|esp32', 49 | '--log-cli-level', 50 | 'DEBUG', 51 | ) 52 | 53 | result.assert_outcomes(passed=2) 54 | 55 | esp32s2_set_cache_index = first_index_of_messages( 56 | re.compile('^set port-target cache: .+ - esp32s2$', re.MULTILINE), caplog.messages 57 | ) 58 | esp32_set_cache_index = first_index_of_messages( 59 | re.compile('^set port-target cache: .+ - esp32$', re.MULTILINE), caplog.messages, esp32s2_set_cache_index + 1 60 | ) 61 | esp32s2_hit_cache_index = first_index_of_messages( 62 | re.compile('^hit port-target cache: .+ - esp32s2$', re.MULTILINE), caplog.messages, esp32_set_cache_index + 1 63 | ) 64 | first_index_of_messages( 65 | re.compile('^hit port-target cache: .+ - esp32$', re.MULTILINE), caplog.messages, esp32s2_hit_cache_index + 1 66 | ) 67 | 68 | 69 | def test_detect_port_with_local_cache(testdir): 70 | pytest.global_port_target_cache = {} 71 | 72 | testdir.makepyfile(r""" 73 | import pytest 74 | 75 | def test_empty_port_target_cache_before_init_devices(port_target_cache): 76 | assert isinstance(port_target_cache, dict) 77 | assert port_target_cache == pytest.global_port_target_cache 78 | 79 | def test_init_devices(dut, port_target_cache): 80 | pytest.global_port_target_cache = port_target_cache 81 | 82 | def test_empty_port_target_cache_after_init_devices(port_target_cache): 83 | assert port_target_cache == pytest.global_port_target_cache 84 | """) 85 | result = testdir.runpytest( 86 | '-s', 87 | '--embedded-services', 88 | 'esp', 89 | '--cache-dir', 90 | './cache-test', 91 | '--count', 92 | '2', 93 | ) 94 | result.assert_outcomes(passed=3) 95 | 96 | testdir.makepyfile(""" 97 | import pytest 98 | 99 | def test_load_local_saved_port_target_cache(port_target_cache): 100 | assert port_target_cache == pytest.global_port_target_cache 101 | """) 102 | result = testdir.runpytest( 103 | '-s', 104 | '--embedded-services', 105 | 'esp', 106 | '--cache-dir', 107 | './cache-test', 108 | '--count', 109 | '2', 110 | ) 111 | result.assert_outcomes(passed=1) 112 | -------------------------------------------------------------------------------- /pytest-embedded-serial/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-serial/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-serial 2 | 3 | pytest embedded service for testing via serial ports 4 | 5 | Extra Functionalities: 6 | 7 | - `serial`: enable the fixture 8 | - `dut`: duplicate the `serial` output to `pexpect_proc`. 9 | -------------------------------------------------------------------------------- /pytest-embedded-serial/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-serial" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Framework :: Pytest", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | ] 30 | dynamic = ["version", "description"] 31 | requires-python = ">=3.7" 32 | 33 | dependencies = [ 34 | "pytest-embedded~=1.16.1", 35 | "pyserial~=3.0", 36 | ] 37 | 38 | [project.urls] 39 | homepage = "https://github.com/espressif/pytest-embedded" 40 | repository = "https://github.com/espressif/pytest-embedded" 41 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 42 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 43 | -------------------------------------------------------------------------------- /pytest-embedded-serial/pytest_embedded_serial/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with Serial.""" 2 | 3 | from .dut import SerialDut 4 | from .serial import Serial 5 | 6 | __all__ = [ 7 | 'Serial', 8 | 'SerialDut', 9 | ] 10 | 11 | __version__ = '1.16.1' 12 | -------------------------------------------------------------------------------- /pytest-embedded-serial/pytest_embedded_serial/dut.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, AnyStr, Optional 2 | 3 | from pytest_embedded.dut import Dut 4 | from pytest_embedded.utils import to_bytes 5 | 6 | from .serial import Serial 7 | 8 | if TYPE_CHECKING: 9 | from pytest_embedded_jtag import Gdb, OpenOcd, Telnet 10 | 11 | 12 | class SerialDut(Dut): 13 | """ 14 | Dut class for serial ports 15 | 16 | Attributes: 17 | serial (Serial): `Serial` instance 18 | openocd (OpenOcd): `OpenOcd` instance, applied only when `jtag` service is activated 19 | gdb (Gdb): `Gdb` instance, applied only when `jtag` service is activated 20 | telnet (Telnet): `Telnet` instance, applied only when `jtag` service is activated 21 | """ 22 | 23 | def __init__( 24 | self, 25 | serial: Serial, 26 | openocd: Optional['OpenOcd'] = None, 27 | gdb: Optional['Gdb'] = None, 28 | telnet: Optional['Telnet'] = None, 29 | **kwargs, 30 | ) -> None: 31 | super().__init__(**kwargs) 32 | 33 | self.serial = serial 34 | self.openocd = openocd 35 | self.gdb = gdb 36 | self.telnet = telnet 37 | 38 | self.setup_jtag() 39 | 40 | def write(self, data: AnyStr) -> None: 41 | self.serial.proc.write(to_bytes(data, '\n')) 42 | 43 | def setup_jtag(self): 44 | if self.gdb: 45 | self.gdb.write('set remotetimeout 10') 46 | self.gdb.write(f'target extended-remote :{self.openocd.gdb_port}') 47 | -------------------------------------------------------------------------------- /pytest-embedded-serial/tests/test_serial.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | 6 | def test_custom_serial_device(testdir): 7 | testdir.makepyfile(r""" 8 | import pytest 9 | 10 | def test_serial_mixed(dut): 11 | from pytest_embedded.dut_factory import DutFactory 12 | assert len(dut)==2 13 | another_dut = DutFactory.create() 14 | st = set( 15 | ( 16 | dut[0].serial.port, 17 | dut[1].serial.port, 18 | another_dut.serial.port 19 | ) 20 | ) 21 | assert len(st) == 3 22 | 23 | def test_custom_dut(): 24 | from pytest_embedded.dut_factory import DutFactory 25 | another_dut = DutFactory.create(embedded_services='esp,serial') 26 | """) 27 | 28 | result = testdir.runpytest( 29 | '-s', 30 | '--embedded-services', 31 | 'esp,serial', 32 | '--count', 33 | 2, 34 | ) 35 | result.assert_outcomes(passed=2, errors=0) 36 | 37 | 38 | def test_custom_serial_device_dut_count_1(testdir): 39 | testdir.makepyfile(r""" 40 | import pytest 41 | 42 | def test_serial_device_created_dut_count_1(dut): 43 | from pytest_embedded.dut_factory import DutFactory 44 | another_dut = DutFactory.create() 45 | another_dut2 = DutFactory.create() 46 | st = set( 47 | ( 48 | dut.serial.port, 49 | another_dut.serial.port, 50 | another_dut2.serial.port 51 | ) 52 | ) 53 | assert len(st) == 3 54 | 55 | 56 | """) 57 | 58 | result = testdir.runpytest( 59 | '-s', 60 | '--embedded-services', 61 | 'esp,serial', 62 | '--count', 63 | 1, 64 | ) 65 | result.assert_outcomes(passed=1, errors=0) 66 | 67 | 68 | @pytest.mark.skipif(sys.platform == 'win32', reason='No socat support on windows') 69 | @pytest.mark.flaky(reruns=3, reruns_delay=2) 70 | def test_serial_port(testdir): 71 | testdir.makepyfile(r""" 72 | import pytest 73 | import subprocess 74 | 75 | @pytest.fixture(autouse=True) 76 | def open_tcp_port(): 77 | proc = subprocess.Popen('socat TCP4-LISTEN:9876,fork EXEC:cat', shell=True) 78 | yield 79 | proc.terminate() 80 | 81 | def test_serial_port(dut): 82 | dut.write(b'hello world\n') 83 | dut.expect('hello world') 84 | """) 85 | 86 | result = testdir.runpytest( 87 | '-s', 88 | '--embedded-services', 89 | 'serial', 90 | '--port', 91 | 'socket://localhost:9876', 92 | ) 93 | 94 | result.assert_outcomes(passed=1) 95 | 96 | 97 | def test_teardown_called_for_multi_dut(testdir): 98 | testdir.makepyfile(r""" 99 | import pytest 100 | 101 | @pytest.mark.parametrize('count, embedded_services, port', [ 102 | ('3', 'serial', '/dev/ttyUSB0|/dev/ttyUSB1|/dev/ttyUSB100'), # set up failure 103 | ], indirect=True) 104 | def test_teardown_called_for_multi_dut_fail(dut): 105 | assert len(dut) == 3 106 | 107 | @pytest.mark.parametrize('count, embedded_services, port', [ 108 | ('3', 'serial', '/dev/ttyUSB0|/dev/ttyUSB1|/dev/ttyUSB2'), # set up succeeded 109 | ], indirect=True) 110 | def test_teardown_called_for_multi_dut_succeeded(dut): 111 | assert dut[0].serial.port == '/dev/ttyUSB0' 112 | assert dut[1].serial.port == '/dev/ttyUSB1' 113 | assert dut[2].serial.port == '/dev/ttyUSB2' 114 | """) 115 | 116 | result = testdir.runpytest() 117 | result.assert_outcomes(passed=1, errors=1) 118 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded-wokwi 2 | 3 | pytest-embedded service for running tests on [Wokwi](https://wokwi.com/ci) instead of the real target. 4 | 5 | Wokwi supports most ESP32 targets, including: esp32, esp32s2, esp32s3, esp32c3, esp32c6, and esp32h2. In addition, it supports a [wide range of peripherals](https://docs.wokwi.com/getting-started/supported-hardware), including sensors, displays, motors, and debugging tools. 6 | 7 | Running the tests with Wokwi requires an internet connection. Your firmware is uploaded to the Wokwi server for the duration of the simulation, but it is not saved on the server. On-premises Wokwi installations are available for enterprise customers. 8 | 9 | #### Wokwi CLI installation 10 | 11 | The Wokwi plugin uses the [Wokwi CLI](https://github.com/wokwi/wokwi-cli) to interact with the wokwi simulation server. You can download the precompiled CLI binaries from the [releases page](https://github.com/wokwi/wokwi-cli/releases). Alternatively, on Linux or Mac OS, you can install the CLI using the following command: 12 | 13 | ```bash 14 | curl -L https://wokwi.com/ci/install.sh | sh 15 | ``` 16 | 17 | And on Windows: 18 | 19 | ```powershell 20 | iwr https://wokwi.com/ci/install.ps1 -useb | iex 21 | ``` 22 | 23 | #### Wokwi API Tokens 24 | 25 | Before using this plugin, you need to create a free Wokwi account and [generate an API key](https://wokwi.com/dashboard/ci). You can then set the `WOKWI_CLI_TOKEN` environment variable to the API key. 26 | 27 | Linux / Mac OS / WSL: 28 | 29 | ```bash 30 | export WOKWI_CLI_TOKEN="your-api-key" 31 | ``` 32 | 33 | Windows PowerShell: 34 | 35 | ```powershell 36 | $env:WOKWI_CLI_TOKEN="your-api-key" 37 | ``` 38 | 39 | #### Usage 40 | 41 | To run your tests with Wokwi, make sure to specify the `wokwi` service when running pytest, e.g.: 42 | 43 | ``` 44 | pytest --embedded-services idf,wokwi 45 | ``` 46 | 47 | To limit the amount of simulation time, use the `--wokwi-timeout` flag. For example, to set the simulation time limit to 60 seconds (60000 milliseconds): 48 | 49 | ``` 50 | pytest --embedded-services idf,wokwi --wokwi-timeout=60000 51 | ``` 52 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded-wokwi" 7 | authors = [ 8 | {name = "Fu Hanxi", email = "fuhanxi@espressif.com"}, 9 | {name = "Uri Shaked", email = "uri@wokwi.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE"} 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: Pytest", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Programming Language :: Python", 29 | "Topic :: Software Development :: Testing", 30 | ] 31 | dynamic = ["version", "description"] 32 | requires-python = ">=3.7" 33 | 34 | dependencies = [ 35 | "pytest-embedded~=1.16.1", 36 | "toml~=0.10.2", 37 | ] 38 | 39 | [project.optional-dependencies] 40 | idf = [ 41 | "pytest-embedded-idf~=1.16.1", 42 | ] 43 | 44 | [project.urls] 45 | homepage = "https://github.com/espressif/pytest-embedded" 46 | repository = "https://github.com/espressif/pytest-embedded" 47 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 48 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 49 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pytest_embedded_wokwi/__init__.py: -------------------------------------------------------------------------------- 1 | """Make pytest-embedded plugin work with the Wokwi CLI.""" 2 | 3 | WOKWI_CLI_MINIMUM_VERSION = '0.10.1' 4 | 5 | from .dut import WokwiDut # noqa 6 | from .wokwi_cli import WokwiCLI # noqa 7 | 8 | __all__ = [ 9 | 'WOKWI_CLI_MINIMUM_VERSION', 10 | 'WokwiCLI', 11 | 'WokwiDut', 12 | ] 13 | 14 | __version__ = '1.16.1' 15 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pytest_embedded_wokwi/arduino.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from pathlib import Path 3 | 4 | if t.TYPE_CHECKING: 5 | from pytest_embedded_arduino.app import ArduinoApp 6 | 7 | 8 | class ArduinoFirmwareResolver: 9 | """ 10 | ArduinoFirmwareResolver class 11 | """ 12 | 13 | def resolve_firmware(self, app: 'ArduinoApp'): 14 | # get path of ino.bin file 15 | return Path(app.binary_path, app.sketch + '.ino.merged.bin') 16 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pytest_embedded_wokwi/dut.py: -------------------------------------------------------------------------------- 1 | from typing import AnyStr 2 | 3 | from pytest_embedded.dut import Dut 4 | 5 | from .wokwi_cli import WokwiCLI 6 | 7 | 8 | class WokwiDut(Dut): 9 | """ 10 | Wokwi DUT class 11 | """ 12 | 13 | def __init__( 14 | self, 15 | wokwi: WokwiCLI, 16 | **kwargs, 17 | ) -> None: 18 | self.wokwi = wokwi 19 | 20 | super().__init__(**kwargs) 21 | 22 | self._hard_reset_func = self.wokwi._hard_reset 23 | 24 | def write(self, s: AnyStr) -> None: 25 | self.wokwi.write(s) 26 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pytest_embedded_wokwi/idf.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from pathlib import Path 3 | 4 | if t.TYPE_CHECKING: 5 | from pytest_embedded_idf.app import IdfApp 6 | 7 | 8 | class IDFFirmwareResolver: 9 | """ 10 | IDFFirmwareResolver class 11 | """ 12 | 13 | def resolve_firmware(self, app: 'IdfApp'): 14 | return Path(app.binary_path, 'flasher_args.json') 15 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import re 5 | import shutil 6 | import subprocess 7 | import typing as t 8 | from pathlib import Path 9 | 10 | import toml 11 | from packaging.version import Version 12 | from pytest_embedded import __version__ 13 | from pytest_embedded.log import DuplicateStdoutPopen 14 | 15 | from pytest_embedded_wokwi import WOKWI_CLI_MINIMUM_VERSION 16 | 17 | from .idf import IDFFirmwareResolver 18 | 19 | if t.TYPE_CHECKING: 20 | from pytest_embedded_idf.app import IdfApp 21 | 22 | 23 | target_to_board = { 24 | 'esp32': 'board-esp32-devkit-c-v4', 25 | 'esp32c3': 'board-esp32-c3-devkitm-1', 26 | 'esp32c6': 'board-esp32-c6-devkitc-1', 27 | 'esp32h2': 'board-esp32-h2-devkitm-1', 28 | 'esp32p4': 'board-esp32-p4-function-ev', 29 | 'esp32s2': 'board-esp32-s2-devkitm-1', 30 | 'esp32s3': 'board-esp32-s3-devkitc-1', 31 | } 32 | 33 | 34 | class WokwiCLI(DuplicateStdoutPopen): 35 | """ 36 | WokwiCLI class 37 | """ 38 | 39 | SOURCE = 'Wokwi' 40 | WOKWI_CLI_PATH = 'wokwi-cli' 41 | 42 | def __init__( 43 | self, 44 | firmware_resolver: IDFFirmwareResolver, 45 | wokwi_cli_path: t.Optional[str] = None, 46 | wokwi_timeout: t.Optional[int] = None, 47 | wokwi_scenario: t.Optional[str] = None, 48 | wokwi_diagram: t.Optional[str] = None, 49 | app: t.Optional['IdfApp'] = None, 50 | **kwargs, 51 | ): 52 | """ 53 | Args: 54 | wokwi_cli_path: Wokwi CLI arguments 55 | """ 56 | self.app = app 57 | self.firmware_resolver = firmware_resolver 58 | 59 | # first need to check if wokwi-cli exists in PATH 60 | if shutil.which('wokwi-cli') is None: 61 | raise RuntimeError('Please install wokwi-cli, by running: curl -L https://wokwi.com/ci/install.sh | sh') 62 | 63 | output = subprocess.check_output(['wokwi-cli', '--help']) 64 | try: 65 | wokwi_cli_version = re.match(r'Wokwi CLI v(\d+\.\d+\.\d+)', output.decode('utf-8')).group(1) 66 | except AttributeError: 67 | logging.warning('Failed to get wokwi-cli version, assume version requirements satisfied') 68 | else: 69 | if Version(wokwi_cli_version) < Version(WOKWI_CLI_MINIMUM_VERSION): 70 | raise ValueError( 71 | f'Wokwi CLI version {wokwi_cli_version} is not supported. ' 72 | f'Minimum version required: {WOKWI_CLI_MINIMUM_VERSION}. ' 73 | f'To update Wokwi CLI run: curl -L https://wokwi.com/ci/install.sh | sh' 74 | ) 75 | 76 | self.create_wokwi_toml() 77 | 78 | if wokwi_diagram is None: 79 | self.create_diagram_json() 80 | 81 | wokwi_cli = wokwi_cli_path or self.wokwi_cli_executable 82 | cmd = [wokwi_cli, '--interactive', app.app_path] 83 | if (wokwi_timeout is not None) and (wokwi_timeout > 0): 84 | cmd.extend(['--timeout', str(wokwi_timeout)]) 85 | if (wokwi_scenario is not None) and os.path.exists(wokwi_scenario): 86 | cmd.extend(['--scenario', wokwi_scenario]) 87 | if (wokwi_diagram is not None) and os.path.exists(wokwi_diagram): 88 | cmd.extend(['--diagram-file', wokwi_diagram]) 89 | 90 | super().__init__( 91 | cmd=cmd, 92 | **kwargs, 93 | ) 94 | 95 | @property 96 | def wokwi_cli_executable(self): 97 | return self.WOKWI_CLI_PATH 98 | 99 | def create_wokwi_toml(self): 100 | app = self.app 101 | flasher_args = self.firmware_resolver.resolve_firmware(app) 102 | wokwi_toml_path = os.path.join(app.app_path, 'wokwi.toml') 103 | firmware_path = Path(os.path.relpath(flasher_args, app.app_path)).as_posix() 104 | elf_path = Path(os.path.relpath(app.elf_file, app.app_path)).as_posix() 105 | 106 | if os.path.exists(wokwi_toml_path): 107 | with open(wokwi_toml_path) as f: 108 | toml_data = toml.load(f) 109 | 110 | if 'wokwi' not in toml_data: 111 | toml_data['wokwi'] = {'version': 1} 112 | 113 | wokwi_table = toml_data['wokwi'] 114 | if wokwi_table.get('firmware') == firmware_path and wokwi_table.get('elf') == elf_path: 115 | # No need to update 116 | return 117 | 118 | wokwi_table.update({'firmware': firmware_path, 'elf': elf_path}) 119 | else: 120 | toml_data = { 121 | 'wokwi': { 122 | 'version': 1, 123 | 'generatedBy': f'pytest-embedded-wokwi {__version__}', 124 | 'firmware': firmware_path, 125 | 'elf': elf_path, 126 | } 127 | } 128 | 129 | with open(wokwi_toml_path, 'w') as f: 130 | toml.dump(toml_data, f) 131 | 132 | def create_diagram_json(self): 133 | app = self.app 134 | target_board = target_to_board[app.target] 135 | 136 | # Check for common diagram.json file 137 | diagram_json_path = os.path.join(app.app_path, 'diagram.json') 138 | if os.path.exists(diagram_json_path): 139 | with open(diagram_json_path) as f: 140 | json_data = json.load(f) 141 | if not any(part['type'] == target_board for part in json_data['parts']): 142 | logging.warning( 143 | f'diagram.json exists, no part with type "{target_board}" found. ' 144 | + 'You may need to update the diagram.json file manually to match the target board.' 145 | ) 146 | return 147 | 148 | if app.target == 'esp32p4': 149 | rx_pin = '38' 150 | tx_pin = '37' 151 | else: 152 | rx_pin = 'RX' 153 | tx_pin = 'TX' 154 | 155 | diagram = { 156 | 'version': 1, 157 | 'author': 'Uri Shaked', 158 | 'editor': 'wokwi', 159 | 'parts': [{'type': target_board, 'id': 'esp'}], 160 | 'connections': [ 161 | ['esp:' + tx_pin, '$serialMonitor:RX', ''], 162 | ['esp:' + rx_pin, '$serialMonitor:TX', ''], 163 | ], 164 | } 165 | with open(diagram_json_path, 'w') as f: 166 | f.write(json.dumps(diagram, indent=2)) 167 | 168 | def _hard_reset(self): 169 | """ 170 | This is a fake hard_reset. Keep this API to keep the consistency. 171 | """ 172 | raise NotImplementedError 173 | -------------------------------------------------------------------------------- /pytest-embedded-wokwi/tests/test_wokwi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import pytest 5 | 6 | wokwi_cli_required = pytest.mark.skipif( 7 | shutil.which('wokwi-cli') is None, 8 | reason='Please make sure that `wokwi-cli` is in your PATH env var. ' 9 | + 'To install: https://docs.wokwi.com/wokwi-ci/getting-started#cli-installation', 10 | ) 11 | 12 | wokwi_token_required = pytest.mark.skipif( 13 | os.getenv('WOKWI_CLI_TOKEN') is None, 14 | reason='Please make sure that `WOKWI_CLI_TOKEN` env var is set. Get a token here: https://wokwi.com/dashboard/ci', 15 | ) 16 | 17 | 18 | @wokwi_cli_required 19 | @wokwi_token_required 20 | def test_pexpect_by_wokwi_esp32(testdir): 21 | testdir.makepyfile(""" 22 | import pexpect 23 | import pytest 24 | 25 | def test_pexpect_by_wokwi(dut): 26 | dut.expect('Hello world!') 27 | dut.expect('Restarting') 28 | with pytest.raises(pexpect.TIMEOUT): 29 | dut.expect('foo bar not found', timeout=1) 30 | """) 31 | 32 | result = testdir.runpytest( 33 | '-s', 34 | '--embedded-services', 35 | 'idf,wokwi', 36 | '--app-path', 37 | os.path.join(testdir.tmpdir, 'hello_world_esp32'), 38 | ) 39 | 40 | result.assert_outcomes(passed=1) 41 | 42 | 43 | @wokwi_cli_required 44 | @wokwi_token_required 45 | def test_pexpect_by_wokwi_esp32_arduino(testdir): 46 | testdir.makepyfile(""" 47 | import pexpect 48 | import pytest 49 | def test_pexpect_by_wokwi(dut): 50 | dut.expect('Hello Arduino!') 51 | with pytest.raises(pexpect.TIMEOUT): 52 | dut.expect('foo bar not found', timeout=1) 53 | """) 54 | 55 | result = testdir.runpytest( 56 | '-s', 57 | '--embedded-services', 58 | 'arduino,wokwi', 59 | '--app-path', 60 | os.path.join(testdir.tmpdir, 'hello_world_arduino'), 61 | '--wokwi-diagram', 62 | os.path.join(testdir.tmpdir, 'hello_world_arduino/esp32.diagram.json'), 63 | ) 64 | 65 | result.assert_outcomes(passed=1) 66 | -------------------------------------------------------------------------------- /pytest-embedded/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. 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 | -------------------------------------------------------------------------------- /pytest-embedded/README.md: -------------------------------------------------------------------------------- 1 | ### pytest-embedded 2 | 3 | A pytest plugin for embedded systems. Could activate different services for extra functionalities. 4 | -------------------------------------------------------------------------------- /pytest-embedded/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pytest-embedded" 7 | authors = [{name = "Fu Hanxi", email = "fuhanxi@espressif.com"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Framework :: Pytest", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3 :: Only", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Programming Language :: Python", 26 | "Topic :: Software Development :: Testing", 27 | ] 28 | dynamic = ["version", "description"] 29 | requires-python = ">=3.7" 30 | 31 | dependencies = [ 32 | "pytest>=7.0", 33 | "pexpect>=4.4", 34 | ] 35 | 36 | [project.urls] 37 | homepage = "https://github.com/espressif/pytest-embedded" 38 | repository = "https://github.com/espressif/pytest-embedded" 39 | documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/" 40 | changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md" 41 | 42 | [project.entry-points."pytest11"] 43 | pytest_embedded = "pytest_embedded.plugin" 44 | -------------------------------------------------------------------------------- /pytest-embedded/pytest_embedded/__init__.py: -------------------------------------------------------------------------------- 1 | """A pytest plugin that designed for embedded testing.""" 2 | 3 | from .app import App 4 | from .dut import Dut 5 | from .dut_factory import DutFactory 6 | 7 | __all__ = ['App', 'Dut', 'DutFactory'] 8 | 9 | __version__ = '1.16.1' 10 | -------------------------------------------------------------------------------- /pytest-embedded/pytest_embedded/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Optional 4 | 5 | 6 | class App: 7 | """ 8 | Built binary files base class 9 | 10 | Attributes: 11 | app_path (str): application folder path 12 | binary_path (str): binary folder path 13 | """ 14 | 15 | def __init__( 16 | self, 17 | app_path: Optional[str] = None, 18 | build_dir: Optional[str] = None, 19 | **kwargs, 20 | ): 21 | if app_path is None: 22 | app_path = os.getcwd() 23 | 24 | self.app_path = os.path.realpath(app_path) 25 | self.binary_path = self._get_binary_path(build_dir or 'build') 26 | 27 | for k, v in kwargs.items(): 28 | setattr(self, k, v) 29 | 30 | def _get_binary_path(self, build_dir: Optional[str] = None) -> Optional[str]: 31 | if not build_dir: 32 | return None 33 | 34 | # if build_dir is an absolute path, use it directly 35 | logging.debug(f'checking if {build_dir} is an absolute path...') 36 | if os.path.isabs(build_dir): 37 | return os.path.realpath(build_dir) 38 | 39 | # try relative path based on app_path first 40 | logging.debug(f'checking if {build_dir} exists in {self.app_path}...') 41 | path = os.path.join(self.app_path, build_dir) 42 | if os.path.isdir(path): 43 | return path 44 | 45 | # try relative path based on cwd 46 | logging.debug(f'checking if {build_dir} exists in {os.getcwd()}...') 47 | if os.path.isdir(build_dir): 48 | return os.path.realpath(build_dir) 49 | 50 | logging.debug(f"{path} doesn't exist.") 51 | return None 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This is a requirements.txt for extra dependencies. 2 | # To install all packages, please run `bash foreach.sh install` 3 | 4 | # build 5 | flit 6 | 7 | # testing 8 | pytest-cov 9 | pytest-rerunfailures 10 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | target-version = "py37" 3 | 4 | [lint] 5 | select = [ 6 | 'F', # Pyflakes 7 | 'E', # pycodestyle 8 | 'W', # pycodestyle 9 | 'I', # isort 10 | 'UP', # pyupgrade 11 | 'YTT', # flake8-2020 12 | 'A', # flake8-builtins 13 | 'ARG', # flake8-unused-arguments 14 | 'ERA', # eradicate 15 | 'LOG', # flake8-logging 16 | 'RUF', # Ruff-specific rules 17 | ] 18 | 19 | ignore = [ 20 | 'LOG015', # use pytest logging 21 | ] 22 | 23 | [format] 24 | quote-style = "single" 25 | exclude = [ 26 | "pytest-embedded-jtag/pytest_embedded_jtag/_telnetlib/telnetlib.py" 27 | ] 28 | docstring-code-format = true 29 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 1 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32c2/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 1 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32c3/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 1 2 | #define SOC_B 1 3 | #define SOC_C 1 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32c5/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 1 2 | #define SOC_B 1 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32c6/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 1 2 | #define SOC_B 0 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32c61/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 0 3 | #define SOC_C 1 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32h2/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 1 3 | #define SOC_C 1 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32h21/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 0 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32p4/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 0 3 | #define SOC_C 1 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32s2/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 0 2 | #define SOC_B 0 3 | #define SOC_C 0 4 | -------------------------------------------------------------------------------- /tests/esp-idf/components/soc/esp32s3/include/soc/soc_caps.h: -------------------------------------------------------------------------------- 1 | #define SOC_A 1 2 | #define SOC_B 0 3 | #define SOC_C 1 4 | -------------------------------------------------------------------------------- /tests/esp-idf/tools/cmake/version.cmake: -------------------------------------------------------------------------------- 1 | set(IDF_VERSION_MAJOR 5) 2 | set(IDF_VERSION_MINOR 5) 3 | set(IDF_VERSION_PATCH 0) 4 | 5 | set(ENV{IDF_VERSION} "${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}") 6 | -------------------------------------------------------------------------------- /tests/esp-idf/tools/idf_py_actions/constants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | SUPPORTED_TARGETS = ['esp32', 'esp32s2', 'esp32c3', 'esp32s3', 'esp32c2', 'esp32c6', 'esp32h2', 'esp32p4'] 5 | PREVIEW_TARGETS = ['linux', 'esp32c5', 'esp32c61', 'esp32h21'] 6 | -------------------------------------------------------------------------------- /tests/fixtures/esp32_qemu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/esp32_qemu.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/build/build.options.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalFiles": "", 3 | "builtInLibrariesFolders": "", 4 | "builtInToolsFolders": "/Applications/Arduino.app/Contents/Java/tools-builder", 5 | "compiler.optimization_flags": "-Os", 6 | "customBuildProperties": "", 7 | "fqbn": "espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app", 8 | "hardwareFolders": "/Users/prochy/Documents/Arduino/hardware", 9 | "otherLibrariesFolders": "/Users/prochy/Documents/Arduino/libraries", 10 | "runtime.ide.version": "10810", 11 | "sketchLocation": "/Users/prochy/Documents/Arduino/hardware/espressif/esp32/tests/hello_world/hello_world.ino" 12 | } -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.partitions.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.partitions.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_arduino/esp32.diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "jan.prochazka@espressif.com", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-devkit-c-v4", 8 | "id": "esp" 9 | } 10 | ], 11 | "connections": [ 12 | [ 13 | "esp:TX", 14 | "$serialMonitor:RX", 15 | "" 16 | ], 17 | [ 18 | "esp:RX", 19 | "$serialMonitor:TX", 20 | "" 21 | ] 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "40m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "40m" 9 | }, 10 | "flash_files" : { 11 | "0x1000" : "bootloader/bootloader.bin", 12 | "0x10000" : "hello_world.bin", 13 | "0x8000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x1000", "file" : "bootloader/bootloader.bin", "encrypted" : "false" }, 16 | "app" : { "offset" : "0x10000", "file" : "hello_world.bin", "encrypted" : "false" }, 17 | "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" }, 18 | "extra_esptool_args" : { 19 | "after" : "hard_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/build/hello_world.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32/build/hello_world.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/build/hello_world.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32/build/hello_world.elf -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32/gdbinit: -------------------------------------------------------------------------------- 1 | set pagination off 2 | # Connect to a running instance of OpenOCD 3 | target remote 127.0.0.1:3333 4 | # Reset and halt the target 5 | mon reset halt 6 | # Run to a specific point in ROM code, 7 | # where most of initialization is complete. 8 | thb *0x40007d54 9 | c 10 | # Load the application into RAM 11 | load 12 | # Run till app_main 13 | tb app_main 14 | c 15 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32_flash_enc/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "40m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "40m" 9 | }, 10 | "flash_files" : { 11 | "0x1000" : "bootloader/bootloader.bin", 12 | "0x20000" : "hello_world.bin", 13 | "0xb000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x1000", "file" : "bootloader/bootloader.bin", "encrypted" : "true" }, 16 | "app" : { "offset" : "0x20000", "file" : "hello_world.bin", "encrypted" : "true" }, 17 | "partition-table" : { "offset" : "0xb000", "file" : "partition_table/partition-table.bin", "encrypted" : "true" }, 18 | "extra_esptool_args" : { 19 | "after" : "no_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/build/hello_world.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32_flash_enc/build/hello_world.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32_flash_enc/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/pre_encryption_efuses.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32_flash_enc/pre_encryption_efuses.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32_flash_enc/pre_encryption_key.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32_flash_enc/pre_encryption_key.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "80m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "80m" 9 | }, 10 | "flash_files" : { 11 | "0x0" : "bootloader/bootloader.bin", 12 | "0x10000" : "hello_world.bin", 13 | "0x8000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x0", "file" : "bootloader/bootloader.bin", "encrypted" : "false" }, 16 | "app" : { "offset" : "0x10000", "file" : "hello_world.bin", "encrypted" : "false" }, 17 | "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" }, 18 | "extra_esptool_args" : { 19 | "after" : "hard_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32c3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3/build/hello_world.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3/build/hello_world.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3/build/hello_world.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3/build/hello_world.elf -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3_panic/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "80m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "80m" 9 | }, 10 | "flash_files" : { 11 | "0x0" : "bootloader/bootloader.bin", 12 | "0x10000" : "hello_world.bin", 13 | "0x8000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x0", "file" : "bootloader/bootloader.bin", "encrypted" : "false" }, 16 | "app" : { "offset" : "0x10000", "file" : "hello_world.bin", "encrypted" : "false" }, 17 | "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" }, 18 | "extra_esptool_args" : { 19 | "after" : "hard_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32c3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/hello_world.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3_panic/build/hello_world.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/hello_world.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3_panic/build/hello_world.elf -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_esp32c3_panic/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/build/prefix_map_gdbinit: -------------------------------------------------------------------------------- 1 | set substitute-path /COMPONENT_MAIN_DIR ./hello_world_esp32c3_panic/main 2 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_esp32c3_panic/main/hello_world_main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: CC0-1.0 5 | */ 6 | 7 | #include 8 | #include "sdkconfig.h" 9 | #include "freertos/FreeRTOS.h" 10 | #include "freertos/task.h" 11 | #include "esp_chip_info.h" 12 | #include "esp_flash.h" 13 | 14 | void app_main(void) 15 | { 16 | printf("Hello world!\n"); 17 | *((int*) 0) = 0; 18 | /* Print chip information */ 19 | esp_chip_info_t chip_info; 20 | uint32_t flash_size; 21 | esp_chip_info(&chip_info); 22 | printf("This is %s chip with %d CPU core(s), WiFi%s%s, ", 23 | CONFIG_IDF_TARGET, 24 | chip_info.cores, 25 | (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", 26 | (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); 27 | 28 | printf("silicon revision %d, ", chip_info.revision); 29 | if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) { 30 | printf("Get flash size failed"); 31 | return; 32 | } 33 | 34 | printf("%uMB %s flash\n", flash_size / (1024 * 1024), 35 | (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); 36 | 37 | printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size()); 38 | 39 | for (int i = 10; i >= 0; i--) { 40 | printf("Restarting in %d seconds...\n", i); 41 | vTaskDelay(1000 / portTICK_PERIOD_MS); 42 | } 43 | printf("Restarting now.\n"); 44 | fflush(stdout); 45 | esp_restart(); 46 | } 47 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_linux/build/config/sdkconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_BUILD_TYPE_ELF_RAM": true, 3 | "APP_NO_BLOBS": false, 4 | "APP_REPRODUCIBLE_BUILD": false, 5 | "BOOT_ROM_LOG_ALWAYS_OFF": false, 6 | "BOOT_ROM_LOG_ALWAYS_ON": true, 7 | "BOOT_ROM_LOG_ON_GPIO_HIGH": false, 8 | "BOOT_ROM_LOG_ON_GPIO_LOW": false, 9 | "COMPILER_CXX_EXCEPTIONS": false, 10 | "COMPILER_CXX_RTTI": false, 11 | "COMPILER_DUMP_RTL_FILES": false, 12 | "COMPILER_FLOAT_LIB_FROM_GCCLIB": true, 13 | "COMPILER_HIDE_PATHS_MACROS": true, 14 | "COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE": false, 15 | "COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE": true, 16 | "COMPILER_OPTIMIZATION_ASSERTIONS_SILENT": false, 17 | "COMPILER_OPTIMIZATION_ASSERTION_LEVEL": 2, 18 | "COMPILER_OPTIMIZATION_CHECKS_SILENT": false, 19 | "COMPILER_OPTIMIZATION_DEFAULT": true, 20 | "COMPILER_OPTIMIZATION_NONE": false, 21 | "COMPILER_OPTIMIZATION_PERF": false, 22 | "COMPILER_OPTIMIZATION_SIZE": false, 23 | "COMPILER_STACK_CHECK_MODE_ALL": false, 24 | "COMPILER_STACK_CHECK_MODE_NONE": true, 25 | "COMPILER_STACK_CHECK_MODE_NORM": false, 26 | "COMPILER_STACK_CHECK_MODE_STRONG": false, 27 | "COMPILER_WARN_WRITE_STRINGS": false, 28 | "ESP_CONSOLE_MULTIPLE_UART": true, 29 | "ESP_CONSOLE_NONE": false, 30 | "ESP_CONSOLE_UART": true, 31 | "ESP_CONSOLE_UART_BAUDRATE": 115200, 32 | "ESP_CONSOLE_UART_CUSTOM": false, 33 | "ESP_CONSOLE_UART_DEFAULT": true, 34 | "ESP_CONSOLE_UART_NUM": 0, 35 | "ESP_DEBUG_OCDAWARE": true, 36 | "ESP_DEBUG_STUBS_ENABLE": false, 37 | "ESP_ERR_TO_NAME_LOOKUP": true, 38 | "ESP_INT_WDT": true, 39 | "ESP_INT_WDT_TIMEOUT_MS": 300, 40 | "ESP_IPC_TASK_STACK_SIZE": 1024, 41 | "ESP_MAIN_TASK_AFFINITY": 0, 42 | "ESP_MAIN_TASK_AFFINITY_CPU0": true, 43 | "ESP_MAIN_TASK_AFFINITY_NO_AFFINITY": false, 44 | "ESP_MAIN_TASK_STACK_SIZE": 3584, 45 | "ESP_MINIMAL_SHARED_STACK_SIZE": 2048, 46 | "ESP_PANIC_HANDLER_IRAM": false, 47 | "ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND": true, 48 | "ESP_SLEEP_GPIO_RESET_WORKAROUND": false, 49 | "ESP_SLEEP_MSPI_NEED_ALL_IO_PU": false, 50 | "ESP_SLEEP_POWER_DOWN_FLASH": false, 51 | "ESP_SYSTEM_CHECK_INT_LEVEL_4": true, 52 | "ESP_SYSTEM_EVENT_QUEUE_SIZE": 32, 53 | "ESP_SYSTEM_EVENT_TASK_STACK_SIZE": 2304, 54 | "ESP_SYSTEM_GDBSTUB_RUNTIME": false, 55 | "ESP_SYSTEM_PANIC_GDBSTUB": false, 56 | "ESP_SYSTEM_PANIC_PRINT_HALT": false, 57 | "ESP_SYSTEM_PANIC_PRINT_REBOOT": true, 58 | "ESP_SYSTEM_PANIC_SILENT_REBOOT": false, 59 | "ESP_SYSTEM_SINGLE_CORE_MODE": true, 60 | "ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0": true, 61 | "ESP_TASK_WDT_EN": true, 62 | "ESP_TASK_WDT_INIT": true, 63 | "ESP_TASK_WDT_PANIC": false, 64 | "ESP_TASK_WDT_TIMEOUT_S": 5, 65 | "FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER": true, 66 | "FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE": false, 67 | "FREERTOS_CHECK_STACKOVERFLOW_CANARY": true, 68 | "FREERTOS_CHECK_STACKOVERFLOW_NONE": false, 69 | "FREERTOS_CHECK_STACKOVERFLOW_PTRVAL": false, 70 | "FREERTOS_CORETIMER_SYSTIMER_LVL1": true, 71 | "FREERTOS_CORETIMER_SYSTIMER_LVL3": false, 72 | "FREERTOS_DEBUG_OCDAWARE": true, 73 | "FREERTOS_ENABLE_BACKWARD_COMPATIBILITY": false, 74 | "FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP": false, 75 | "FREERTOS_ENABLE_TASK_SNAPSHOT": true, 76 | "FREERTOS_GENERATE_RUN_TIME_STATS": false, 77 | "FREERTOS_HZ": 100, 78 | "FREERTOS_IDLE_TASK_STACKSIZE": 1536, 79 | "FREERTOS_INTERRUPT_BACKTRACE": true, 80 | "FREERTOS_ISR_STACKSIZE": 1536, 81 | "FREERTOS_MAX_TASK_NAME_LEN": 16, 82 | "FREERTOS_NO_AFFINITY": 2147483647, 83 | "FREERTOS_OPTIMIZED_SCHEDULER": true, 84 | "FREERTOS_PLACE_FUNCTIONS_INTO_FLASH": false, 85 | "FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH": false, 86 | "FREERTOS_QUEUE_REGISTRY_SIZE": 0, 87 | "FREERTOS_SMP": false, 88 | "FREERTOS_SUPPORT_STATIC_ALLOCATION": true, 89 | "FREERTOS_SYSTICK_USES_SYSTIMER": true, 90 | "FREERTOS_TASK_FUNCTION_WRAPPER": true, 91 | "FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES": 1, 92 | "FREERTOS_THREAD_LOCAL_STORAGE_POINTERS": 1, 93 | "FREERTOS_TICK_SUPPORT_SYSTIMER": true, 94 | "FREERTOS_TIMER_QUEUE_LENGTH": 10, 95 | "FREERTOS_TIMER_TASK_PRIORITY": 1, 96 | "FREERTOS_TIMER_TASK_STACK_DEPTH": 2053, 97 | "FREERTOS_TLSP_DELETION_CALLBACKS": true, 98 | "FREERTOS_UNICORE": true, 99 | "FREERTOS_USE_IDLE_HOOK": false, 100 | "FREERTOS_USE_TICK_HOOK": false, 101 | "FREERTOS_USE_TRACE_FACILITY": false, 102 | "FREERTOS_WATCHPOINT_END_OF_STACK": false, 103 | "HAL_ASSERTION_DISABLE": false, 104 | "HAL_ASSERTION_ENABLE": false, 105 | "HAL_ASSERTION_EQUALS_SYSTEM": true, 106 | "HAL_ASSERTION_SILENT": false, 107 | "HAL_DEFAULT_ASSERTION_LEVEL": 2, 108 | "HEAP_ABORT_WHEN_ALLOCATION_FAILS": false, 109 | "HEAP_POISONING_COMPREHENSIVE": false, 110 | "HEAP_POISONING_DISABLED": true, 111 | "HEAP_POISONING_LIGHT": false, 112 | "HEAP_TRACING_OFF": true, 113 | "HEAP_TRACING_STANDALONE": false, 114 | "HEAP_TRACING_TOHOST": false, 115 | "IDF_CMAKE": true, 116 | "IDF_FIRMWARE_CHIP_ID": 65535, 117 | "IDF_TARGET": "linux", 118 | "IDF_TARGET_LINUX": true, 119 | "LOG_COLORS": true, 120 | "LOG_DEFAULT_LEVEL": 3, 121 | "LOG_DEFAULT_LEVEL_DEBUG": false, 122 | "LOG_DEFAULT_LEVEL_ERROR": false, 123 | "LOG_DEFAULT_LEVEL_INFO": true, 124 | "LOG_DEFAULT_LEVEL_NONE": false, 125 | "LOG_DEFAULT_LEVEL_VERBOSE": false, 126 | "LOG_DEFAULT_LEVEL_WARN": false, 127 | "LOG_MAXIMUM_EQUALS_DEFAULT": true, 128 | "LOG_MAXIMUM_LEVEL": 3, 129 | "LOG_MAXIMUM_LEVEL_DEBUG": false, 130 | "LOG_MAXIMUM_LEVEL_VERBOSE": false, 131 | "LOG_TIMESTAMP_SOURCE_RTOS": true, 132 | "LOG_TIMESTAMP_SOURCE_SYSTEM": false, 133 | "MMU_PAGE_MODE": "64KB", 134 | "MMU_PAGE_SIZE": 65536, 135 | "MMU_PAGE_SIZE_64KB": true, 136 | "PERIPH_CTRL_FUNC_IN_IRAM": false 137 | } -------------------------------------------------------------------------------- /tests/fixtures/hello_world_linux/build/hello_world.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_linux/build/hello_world.elf -------------------------------------------------------------------------------- /tests/fixtures/hello_world_linux/build/project_description.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "hello_world", 3 | "project_path": "/home/aleksei/esp/esp-idf/tools/test_apps/linux_compatible/hello_world_linux_compatible", 4 | "build_dir": "/home/aleksei/esp/esp-idf/tools/test_apps/linux_compatible/hello_world_linux_compatible/build", 5 | "config_file": "/home/aleksei/esp/esp-idf/tools/test_apps/linux_compatible/hello_world_linux_compatible/sdkconfig", 6 | "config_defaults": "", 7 | "bootloader_elf": "", 8 | "app_elf": "hello_world.elf", 9 | "app_bin": "hello_world.bin", 10 | "git_revision": "v5.1-dev-2351-gc01f71cfcf", 11 | "target": "linux", 12 | "rev": "", 13 | "min_rev": "", 14 | "max_rev": "", 15 | "phy_data_partition": "", 16 | "monitor_baud" : "", 17 | "monitor_toolprefix": "", 18 | "config_environment" : { 19 | "COMPONENT_KCONFIGS" : "/home/aleksei/esp/esp-idf/components/esp_common/Kconfig;/home/aleksei/esp/esp-idf/components/esp_hw_support/Kconfig;/home/aleksei/esp/esp-idf/components/esp_system/Kconfig;/home/aleksei/esp/esp-idf/components/freertos/Kconfig;/home/aleksei/esp/esp-idf/components/hal/Kconfig;/home/aleksei/esp/esp-idf/components/heap/Kconfig;/home/aleksei/esp/esp-idf/components/log/Kconfig", 20 | "COMPONENT_KCONFIGS_PROJBUILD" : "/home/aleksei/esp/esp-idf/components/esp_rom/Kconfig.projbuild" 21 | }, 22 | "build_components" : [ "esp_common", "esp_hw_support", "esp_rom", "esp_system", "freertos", "hal", "heap", "linux", "log", "main", "soc", "" ], 23 | "build_component_paths" : [ "/home/aleksei/esp/esp-idf/components/esp_common", "/home/aleksei/esp/esp-idf/components/esp_hw_support", "/home/aleksei/esp/esp-idf/components/esp_rom", "/home/aleksei/esp/esp-idf/components/esp_system", "/home/aleksei/esp/esp-idf/components/freertos", "/home/aleksei/esp/esp-idf/components/hal", "/home/aleksei/esp/esp-idf/components/heap", "/home/aleksei/esp/esp-idf/components/linux", "/home/aleksei/esp/esp-idf/components/log", "/home/aleksei/esp/esp-idf/tools/test_apps/linux_compatible/hello_world_linux_compatible/main", "/home/aleksei/esp/esp-idf/components/soc", "" ], 24 | "debug_prefix_map_gdbinit": "" 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/hello_world_nuttx/nuttx.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_nuttx/nuttx.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_nuttx_mcuboot/mcuboot-esp32.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_nuttx_mcuboot/mcuboot-esp32.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_nuttx_mcuboot/nuttx.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_nuttx_mcuboot/nuttx.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_nuttx_qemu/nuttx.merged.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_nuttx_qemu/nuttx.merged.bin -------------------------------------------------------------------------------- /tests/fixtures/hello_world_nuttx_qemu/qemu_efuse.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/hello_world_nuttx_qemu/qemu_efuse.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | set(EXTRA_COMPONENT_DIRS 4 | "$ENV{IDF_PATH}/tools/unit-test-app/components") 5 | 6 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 7 | project(case_tester_example) 8 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/build/case_tester_example.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32/build/case_tester_example.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "40m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "40m" 9 | }, 10 | "flash_files" : { 11 | "0x1000" : "bootloader/bootloader.bin", 12 | "0x10000" : "case_tester_example.bin", 13 | "0x8000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x1000", "file" : "bootloader/bootloader.bin", "encrypted" : "false" }, 16 | "app" : { "offset" : "0x10000", "file" : "case_tester_example.bin", "encrypted" : "false" }, 17 | "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" }, 18 | "extra_esptool_args" : { 19 | "after" : "hard_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "test_app_main.c" 2 | "case_tester_example.c" 3 | INCLUDE_DIRS "." 4 | PRIV_REQUIRES test_utils unity 5 | WHOLE_ARCHIVE) 6 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/main/case_tester_example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "freertos/FreeRTOS.h" 3 | #include "freertos/task.h" 4 | #include "esp_chip_info.h" 5 | #include "hal/rtc_cntl_ll.h" 6 | #include "unity.h" 7 | #include "test_utils.h" 8 | #include "esp_log.h" 9 | 10 | 11 | TEST_CASE("normal_case1", "[normal_case]") 12 | { 13 | esp_chip_info_t chip_info; 14 | esp_chip_info(&chip_info); 15 | ESP_LOGI("normal case1", "This is %s chip with %d CPU core(s), WiFi%s%s, ", 16 | CONFIG_IDF_TARGET, 17 | chip_info.cores, 18 | (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", 19 | (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); 20 | TEST_ASSERT(true); 21 | } 22 | 23 | TEST_CASE("normal_case2", "[normal_case][timeout=10]") 24 | { 25 | ESP_LOGI("normal case2", "delay 3s"); 26 | vTaskDelay(pdMS_TO_TICKS(3000)); 27 | 28 | // cause a crash 29 | volatile uint8_t *test = (uint8_t*)0x0; 30 | *test = 1; 31 | 32 | TEST_ASSERT(true); 33 | } 34 | 35 | void test_stage1(void) 36 | { 37 | ESP_LOGI("multi_stage", "stage1: software restart"); 38 | 39 | vTaskDelay(pdMS_TO_TICKS(100)); 40 | esp_restart(); 41 | } 42 | 43 | void test_stage2(void) 44 | { 45 | ESP_LOGI("multi_stage", "stage2: assert fail"); 46 | vTaskDelay(pdMS_TO_TICKS(100)); 47 | assert(false); // this one will cause a panic, the test won't fail 48 | } 49 | 50 | void test_stage3(void) 51 | { 52 | ESP_LOGI("multi_stage", "stage3: system reset"); 53 | rtc_cntl_ll_reset_system(); 54 | } 55 | 56 | void test_stage4(void) 57 | { 58 | ESP_LOGI("multi_stage", "stage4: finish"); 59 | } 60 | 61 | TEST_CASE_MULTIPLE_STAGES("multiple_stages_test", "[multi_stage]", 62 | test_stage1, test_stage2, test_stage3, test_stage4); 63 | 64 | 65 | void test_dev1(void) 66 | { 67 | ESP_LOGI("multi_dev", "dev1 start"); 68 | unity_send_signal("signal 1 from dev 1"); 69 | unity_wait_for_signal("signal 2 from dev 2"); 70 | unity_send_signal("signal 3 from dev 1"); 71 | 72 | for (int i = 0; i < 10; i++) { 73 | unity_wait_for_signal("continuous signal"); 74 | } 75 | } 76 | 77 | void test_dev2(void) 78 | { 79 | ESP_LOGI("multi_dev", "dev2 start"); 80 | unity_wait_for_signal("signal 1 from dev 1"); 81 | unity_send_signal("signal 2 from dev 2"); 82 | unity_wait_for_signal("signal 3 from dev 1"); 83 | for (int i = 0; i < 10; i++) { 84 | unity_send_signal("continuous signal"); 85 | } 86 | } 87 | 88 | TEST_CASE_MULTIPLE_DEVICES("multiple_devices_test", "[multi_dev][timeout=150]", 89 | test_dev1, test_dev2); 90 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32/main/test_app_main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "unity.h" 8 | #include "unity_test_runner.h" 9 | #include "esp_heap_caps.h" 10 | 11 | #define TEST_MEMORY_LEAK_THRESHOLD (-200) 12 | 13 | static size_t before_free_8bit; 14 | static size_t before_free_32bit; 15 | 16 | static void check_leak(size_t before_free, size_t after_free, const char *type) 17 | { 18 | ssize_t delta = after_free - before_free; 19 | printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta); 20 | TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak"); 21 | } 22 | 23 | void setUp(void) 24 | { 25 | before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); 26 | before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); 27 | } 28 | 29 | void tearDown(void) 30 | { 31 | size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); 32 | size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); 33 | check_leak(before_free_8bit, after_free_8bit, "8BIT"); 34 | check_leak(before_free_32bit, after_free_32bit, "32BIT"); 35 | } 36 | 37 | void app_main(void) 38 | { 39 | unity_run_menu(); 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | set(EXTRA_COMPONENT_DIRS 4 | "$ENV{IDF_PATH}/tools/unit-test-app/components") 5 | 6 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 7 | project(case_tester_example) 8 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/build/flasher_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "write_flash_args" : [ "--flash_mode", "dio", 3 | "--flash_size", "2MB", 4 | "--flash_freq", "80m" ], 5 | "flash_settings" : { 6 | "flash_mode": "dio", 7 | "flash_size": "2MB", 8 | "flash_freq": "80m" 9 | }, 10 | "flash_files" : { 11 | "0x0" : "bootloader/bootloader.bin", 12 | "0x10000" : "case_tester_example.bin", 13 | "0x8000" : "partition_table/partition-table.bin" 14 | }, 15 | "bootloader" : { "offset" : "0x0", "file" : "bootloader/bootloader.bin", "encrypted" : "false" }, 16 | "app" : { "offset" : "0x10000", "file" : "case_tester_example.bin", "encrypted" : "false" }, 17 | "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" }, 18 | "extra_esptool_args" : { 19 | "after" : "hard_reset", 20 | "before" : "default_reset", 21 | "stub" : true, 22 | "chip" : "esp32c3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/build/partition_table/partition-table.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_esp32c3/build/partition_table/partition-table.bin -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "test_app_main.c" 2 | "case_tester_example.c" 3 | INCLUDE_DIRS "." 4 | PRIV_REQUIRES test_utils unity 5 | WHOLE_ARCHIVE) 6 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/main/case_tester_example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "freertos/FreeRTOS.h" 3 | #include "freertos/task.h" 4 | #include "esp_chip_info.h" 5 | #include "hal/rtc_cntl_ll.h" 6 | #include "unity.h" 7 | #include "test_utils.h" 8 | #include "esp_log.h" 9 | 10 | 11 | TEST_CASE("normal_case1", "[normal_case]") 12 | { 13 | esp_chip_info_t chip_info; 14 | esp_chip_info(&chip_info); 15 | ESP_LOGI("normal case1", "This is %s chip with %d CPU core(s), WiFi%s%s, ", 16 | CONFIG_IDF_TARGET, 17 | chip_info.cores, 18 | (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", 19 | (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); 20 | TEST_ASSERT(true); 21 | } 22 | 23 | TEST_CASE("normal_case2", "[normal_case][timeout=10]") 24 | { 25 | ESP_LOGI("normal case2", "delay 3s"); 26 | vTaskDelay(pdMS_TO_TICKS(3000)); 27 | 28 | // cause a crash 29 | volatile uint8_t *test = (uint8_t*)0x0; 30 | *test = 1; 31 | 32 | TEST_ASSERT(true); 33 | } 34 | 35 | void test_stage1(void) 36 | { 37 | ESP_LOGI("multi_stage", "stage1: software restart"); 38 | 39 | vTaskDelay(pdMS_TO_TICKS(100)); 40 | esp_restart(); 41 | } 42 | 43 | void test_stage2(void) 44 | { 45 | ESP_LOGI("multi_stage", "stage2: assert fail"); 46 | vTaskDelay(pdMS_TO_TICKS(100)); 47 | assert(false); 48 | } 49 | 50 | void test_stage3(void) 51 | { 52 | ESP_LOGI("multi_stage", "stage3: system reset"); 53 | rtc_cntl_ll_reset_system(); 54 | } 55 | 56 | void test_stage4(void) 57 | { 58 | ESP_LOGI("multi_stage", "stage4: finish"); 59 | } 60 | 61 | TEST_CASE_MULTIPLE_STAGES("multiple_stages_test", "[multi_stage]", 62 | test_stage1, test_stage2, test_stage3, test_stage4); 63 | 64 | 65 | void test_dev1(void) 66 | { 67 | ESP_LOGI("multi_dev", "dev1 start"); 68 | unity_send_signal("signal 1 from dev 1"); 69 | unity_wait_for_signal("signal 2 from dev 2"); 70 | unity_send_signal("signal 3 from dev 1"); 71 | 72 | for (int i = 0; i < 10; i++) { 73 | unity_wait_for_signal("continuous signal"); 74 | } 75 | } 76 | 77 | void test_dev2(void) 78 | { 79 | ESP_LOGI("multi_dev", "dev2 start"); 80 | unity_wait_for_signal("signal 1 from dev 1"); 81 | unity_send_signal("signal 2 from dev 2"); 82 | unity_wait_for_signal("signal 3 from dev 1"); 83 | for (int i = 0; i < 10; i++) { 84 | unity_send_signal("continuous signal"); 85 | } 86 | } 87 | 88 | TEST_CASE_MULTIPLE_DEVICES("multiple_devices_test", "[multi_dev][timeout=150]", 89 | test_dev1, test_dev2); 90 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_esp32c3/main/test_app_main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "unity.h" 8 | #include "unity_test_runner.h" 9 | #include "esp_heap_caps.h" 10 | 11 | #define TEST_MEMORY_LEAK_THRESHOLD (-200) 12 | 13 | static size_t before_free_8bit; 14 | static size_t before_free_32bit; 15 | 16 | static void check_leak(size_t before_free, size_t after_free, const char *type) 17 | { 18 | ssize_t delta = after_free - before_free; 19 | printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta); 20 | TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak"); 21 | } 22 | 23 | void setUp(void) 24 | { 25 | before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); 26 | before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); 27 | } 28 | 29 | void tearDown(void) 30 | { 31 | size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); 32 | size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); 33 | check_leak(before_free_8bit, after_free_8bit, "8BIT"); 34 | check_leak(before_free_32bit, after_free_32bit, "32BIT"); 35 | } 36 | 37 | void app_main(void) 38 | { 39 | unity_run_menu(); 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/unit_test_app_linux/build/test_esp_event.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/pytest-embedded/5f412f1280d432ecfff39d25792fb9400832439b/tests/fixtures/unit_test_app_linux/build/test_esp_event.elf --------------------------------------------------------------------------------