idf-wokwi 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idf_wokwi-1.0.0/.editorconfig +19 -0
- idf_wokwi-1.0.0/.github/workflows/ci.yaml +93 -0
- idf_wokwi-1.0.0/.github/workflows/release.yaml +38 -0
- idf_wokwi-1.0.0/.gitignore +10 -0
- idf_wokwi-1.0.0/.pre-commit-config.yaml +24 -0
- idf_wokwi-1.0.0/.ruff.toml +23 -0
- idf_wokwi-1.0.0/CHANGELOG.md +5 -0
- idf_wokwi-1.0.0/CONTRIBUTING.md +30 -0
- idf_wokwi-1.0.0/PKG-INFO +64 -0
- idf_wokwi-1.0.0/README.md +39 -0
- idf_wokwi-1.0.0/mypy.ini +14 -0
- idf_wokwi-1.0.0/pyproject.toml +45 -0
- idf_wokwi-1.0.0/src/idf_wokwi/__init__.py +2 -0
- idf_wokwi-1.0.0/src/idf_wokwi/_log.py +14 -0
- idf_wokwi-1.0.0/src/idf_wokwi/diagram.py +56 -0
- idf_wokwi-1.0.0/src/idf_wokwi/expect_engine.py +83 -0
- idf_wokwi-1.0.0/src/idf_wokwi/interactive.py +50 -0
- idf_wokwi-1.0.0/src/idf_wokwi/wokwi_ext.py +185 -0
- idf_wokwi-1.0.0/test-project/CMakeLists.txt +3 -0
- idf_wokwi-1.0.0/test-project/main/CMakeLists.txt +1 -0
- idf_wokwi-1.0.0/test-project/main/main.c +13 -0
- idf_wokwi-1.0.0/tests/__init__.py +0 -0
- idf_wokwi-1.0.0/tests/test_integration.py +124 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# EditorConfig helps maintain consistent coding styles between editors & IDEs
|
|
2
|
+
# https://editorconfig.org
|
|
3
|
+
root = true
|
|
4
|
+
|
|
5
|
+
[*]
|
|
6
|
+
charset = utf-8
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 4
|
|
10
|
+
insert_final_newline = true
|
|
11
|
+
trim_trailing_whitespace = true
|
|
12
|
+
|
|
13
|
+
# Keep README & docs within the same column width as code
|
|
14
|
+
[*.{py,toml,yaml,yml,md}]
|
|
15
|
+
max_line_length = 100
|
|
16
|
+
|
|
17
|
+
# Make makes you use tabs
|
|
18
|
+
[Makefile]
|
|
19
|
+
indent_style = tab
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
ESP_IDF_BRANCH: release/v6.0
|
|
12
|
+
ESP_IDF_TARGET: esp32
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- run: pip install hatch
|
|
23
|
+
- run: hatch run ruff format --check .
|
|
24
|
+
- run: hatch run ruff check .
|
|
25
|
+
- run: hatch run mypy .
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
strategy:
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
os: [ubuntu-24.04, windows-latest]
|
|
32
|
+
python-version: ["3.9", "3.12", "3.13"]
|
|
33
|
+
|
|
34
|
+
runs-on: ${{ matrix.os }}
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v6
|
|
37
|
+
- uses: actions/setup-python@v6
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python-version }}
|
|
40
|
+
- run: pip install hatch
|
|
41
|
+
- run: hatch run dev:pytest tests/test_integration.py::test_action_extensions_structure
|
|
42
|
+
|
|
43
|
+
integration:
|
|
44
|
+
runs-on: ubuntu-24.04
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v6
|
|
47
|
+
- uses: actions/setup-python@v6
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
|
|
51
|
+
- uses: actions/cache@v4
|
|
52
|
+
id: cache-esp-idf
|
|
53
|
+
with:
|
|
54
|
+
path: |
|
|
55
|
+
~/esp-idf
|
|
56
|
+
~/.espressif
|
|
57
|
+
key: esp-idf-${{ env.ESP_IDF_BRANCH }}-${{ env.ESP_IDF_TARGET }}
|
|
58
|
+
|
|
59
|
+
- name: Install ESP-IDF
|
|
60
|
+
if: steps.cache-esp-idf.outputs.cache-hit != 'true'
|
|
61
|
+
run: |
|
|
62
|
+
git clone --depth 1 --recursive --shallow-submodules -b $ESP_IDF_BRANCH https://github.com/espressif/esp-idf.git ~/esp-idf
|
|
63
|
+
~/esp-idf/install.sh $ESP_IDF_TARGET
|
|
64
|
+
|
|
65
|
+
- uses: actions/cache@v4
|
|
66
|
+
id: cache-build
|
|
67
|
+
with:
|
|
68
|
+
path: test-project/build
|
|
69
|
+
key: test-build-${{ env.ESP_IDF_TARGET }}-${{ hashFiles('test-project/**') }}-${{ env.ESP_IDF_BRANCH }}
|
|
70
|
+
|
|
71
|
+
- name: Build test project
|
|
72
|
+
if: steps.cache-build.outputs.cache-hit != 'true'
|
|
73
|
+
run: |
|
|
74
|
+
. ~/esp-idf/export.sh
|
|
75
|
+
cd test-project
|
|
76
|
+
idf.py set-target $ESP_IDF_TARGET
|
|
77
|
+
idf.py build
|
|
78
|
+
|
|
79
|
+
- name: Install idf-wokwi
|
|
80
|
+
run: |
|
|
81
|
+
. ~/esp-idf/export.sh
|
|
82
|
+
pip install .
|
|
83
|
+
|
|
84
|
+
- uses: wokwi/wokwi-ci-server-action@v1
|
|
85
|
+
|
|
86
|
+
- name: Test idf.py wokwi
|
|
87
|
+
run: |
|
|
88
|
+
. ~/esp-idf/export.sh
|
|
89
|
+
cd test-project
|
|
90
|
+
idf.py wokwi --timeout 10000 --expect-text "Hello world!" 2>&1 | tee output.log
|
|
91
|
+
grep -q "Expected text found: Hello world!" output.log
|
|
92
|
+
env:
|
|
93
|
+
WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # OIDC token for trusted publishing
|
|
10
|
+
contents: write # For the release action
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
|
+
environment: pypi
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Build wheel & sdist
|
|
24
|
+
run: |
|
|
25
|
+
pip install --quiet hatch
|
|
26
|
+
hatch build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
verbose: true
|
|
32
|
+
|
|
33
|
+
- uses: softprops/action-gh-release@v2
|
|
34
|
+
with:
|
|
35
|
+
generate_release_notes: true
|
|
36
|
+
files: |
|
|
37
|
+
dist/*.whl
|
|
38
|
+
dist/*.tar.gz
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
# Ruff: formatter + linter
|
|
3
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
4
|
+
rev: v0.12.0
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff-format
|
|
7
|
+
- id: ruff
|
|
8
|
+
args: [--fix]
|
|
9
|
+
|
|
10
|
+
# mypy (strict)
|
|
11
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
12
|
+
rev: v1.16.1
|
|
13
|
+
hooks:
|
|
14
|
+
- id: mypy
|
|
15
|
+
|
|
16
|
+
# Basic hygiene hooks (official, maintained)
|
|
17
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
18
|
+
rev: v5.0.0
|
|
19
|
+
hooks:
|
|
20
|
+
- id: end-of-file-fixer
|
|
21
|
+
- id: trailing-whitespace
|
|
22
|
+
|
|
23
|
+
default_language_version:
|
|
24
|
+
python: python3.12
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# https://docs.astral.sh/ruff/configuration/
|
|
2
|
+
target-version = "py39"
|
|
3
|
+
line-length = 100
|
|
4
|
+
src = ["src", "tests"]
|
|
5
|
+
|
|
6
|
+
# Enable rule bundles
|
|
7
|
+
lint.select = [
|
|
8
|
+
"E", # pycodestyle errors
|
|
9
|
+
"F", # pyflakes
|
|
10
|
+
"I", # isort
|
|
11
|
+
"UP", # pyupgrade
|
|
12
|
+
"PL", # pylint-worthwhile subset
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
lint.ignore = [
|
|
16
|
+
"E501", # handled by formatter
|
|
17
|
+
"PLC0415", # lazy imports for platform-specific modules are fine
|
|
18
|
+
"PLR0913", # callback signature dictated by idf.py extension API
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Per-folder overrides --------------------------------------------------------
|
|
22
|
+
[lint.per-file-ignores]
|
|
23
|
+
"tests/**/*.py" = ["PLR2004"] # magic-numbers okay in tests
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install hatch
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
Lint and type-check:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
hatch run ruff format --check .
|
|
15
|
+
hatch run ruff check .
|
|
16
|
+
hatch run mypy .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run tests:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
hatch run dev:pytest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Pre-commit hooks
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pre-commit
|
|
29
|
+
pre-commit install
|
|
30
|
+
```
|
idf_wokwi-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: idf-wokwi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Wokwi simulation extension for ESP-IDF (idf.py wokwi)
|
|
5
|
+
Project-URL: Source, https://github.com/wokwi/idf-wokwi
|
|
6
|
+
Project-URL: Issues, https://github.com/wokwi/idf-wokwi/issues
|
|
7
|
+
Author-email: Uri Shaked <uri@wokwi.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: esp-idf,esp32,idf.py,simulation,wokwi
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
20
|
+
Classifier: Topic :: System :: Emulators
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: wokwi-client>=0.1.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# idf-wokwi
|
|
27
|
+
|
|
28
|
+
Wokwi simulation extension for ESP-IDF. Adds `idf.py wokwi` command.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install idf-wokwi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Set `WOKWI_CLI_TOKEN` to your [Wokwi API token](https://wokwi.com/dashboard/ci?utm_source=idf-wokwi).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export WOKWI_CLI_TOKEN="your-token-here"
|
|
42
|
+
|
|
43
|
+
# Build and simulate
|
|
44
|
+
idf.py build
|
|
45
|
+
idf.py wokwi
|
|
46
|
+
|
|
47
|
+
# CI mode: exit when expected text appears
|
|
48
|
+
idf.py wokwi --timeout 10000 --expect-text "Hello world!"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Option | Description |
|
|
54
|
+
| ---------------- | -------------------------------------------------------------- |
|
|
55
|
+
| `--diagram-file` | Path to `diagram.json` (defaults to project root) |
|
|
56
|
+
| `--timeout` | Simulation timeout in milliseconds (exit code 42 on timeout) |
|
|
57
|
+
| `--expect-text` | Exit successfully when this text appears in serial output |
|
|
58
|
+
| `--fail-text` | Exit with error when this text appears in serial output |
|
|
59
|
+
| `--expect-regex` | Exit successfully when this regex matches a serial output line |
|
|
60
|
+
| `--fail-regex` | Exit with error when this regex matches a serial output line |
|
|
61
|
+
|
|
62
|
+
## Contributing
|
|
63
|
+
|
|
64
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# idf-wokwi
|
|
2
|
+
|
|
3
|
+
Wokwi simulation extension for ESP-IDF. Adds `idf.py wokwi` command.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install idf-wokwi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Set `WOKWI_CLI_TOKEN` to your [Wokwi API token](https://wokwi.com/dashboard/ci?utm_source=idf-wokwi).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export WOKWI_CLI_TOKEN="your-token-here"
|
|
17
|
+
|
|
18
|
+
# Build and simulate
|
|
19
|
+
idf.py build
|
|
20
|
+
idf.py wokwi
|
|
21
|
+
|
|
22
|
+
# CI mode: exit when expected text appears
|
|
23
|
+
idf.py wokwi --timeout 10000 --expect-text "Hello world!"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
| Option | Description |
|
|
29
|
+
| ---------------- | -------------------------------------------------------------- |
|
|
30
|
+
| `--diagram-file` | Path to `diagram.json` (defaults to project root) |
|
|
31
|
+
| `--timeout` | Simulation timeout in milliseconds (exit code 42 on timeout) |
|
|
32
|
+
| `--expect-text` | Exit successfully when this text appears in serial output |
|
|
33
|
+
| `--fail-text` | Exit with error when this text appears in serial output |
|
|
34
|
+
| `--expect-regex` | Exit successfully when this regex matches a serial output line |
|
|
35
|
+
| `--fail-regex` | Exit with error when this regex matches a serial output line |
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
39
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
idf_wokwi-1.0.0/mypy.ini
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[mypy]
|
|
2
|
+
python_version = 3.9
|
|
3
|
+
strict = True
|
|
4
|
+
show_error_codes = True
|
|
5
|
+
pretty = True
|
|
6
|
+
warn_unused_ignores = True
|
|
7
|
+
warn_return_any = True
|
|
8
|
+
warn_unreachable = True
|
|
9
|
+
|
|
10
|
+
[mypy-wokwi_client.*]
|
|
11
|
+
ignore_missing_imports = True
|
|
12
|
+
|
|
13
|
+
[mypy-pytest.*]
|
|
14
|
+
ignore_missing_imports = True
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "idf-wokwi"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Wokwi simulation extension for ESP-IDF (idf.py wokwi)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Uri Shaked", email = "uri@wokwi.com" }]
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
keywords = ["wokwi", "esp-idf", "simulation", "esp32", "idf.py"]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
25
|
+
"Topic :: System :: Emulators",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["wokwi-client>=0.1.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Source = "https://github.com/wokwi/idf-wokwi"
|
|
32
|
+
Issues = "https://github.com/wokwi/idf-wokwi/issues"
|
|
33
|
+
|
|
34
|
+
[project.entry-points.idf_extension]
|
|
35
|
+
wokwi = "idf_wokwi.wokwi_ext:action_extensions"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.envs.default]
|
|
38
|
+
dependencies = ["ruff>=0.12.0", "mypy>=1.16.1"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.envs.dev]
|
|
41
|
+
template = "default"
|
|
42
|
+
extra-dependencies = ["pytest"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
addopts = "--strict-markers"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def red_print(msg: str) -> None:
|
|
10
|
+
print(f"\033[31m{msg}\033[0m", file=sys.stderr)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def yellow_print(msg: str) -> None:
|
|
14
|
+
print(f"\033[33m{msg}\033[0m", file=sys.stderr)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
"""Diagram generation and discovery for Wokwi simulations."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from ._log import red_print
|
|
12
|
+
|
|
13
|
+
TARGET_WOKWI_PARTS: dict[str, str] = {
|
|
14
|
+
"esp32": "board-esp32-devkit-c-v4",
|
|
15
|
+
"esp32s2": "board-esp32-s2-devkitm-1",
|
|
16
|
+
"esp32s3": "board-esp32-s3-devkitc-1",
|
|
17
|
+
"esp32c3": "board-esp32-c3-devkitm-1",
|
|
18
|
+
"esp32c5": "board-esp32-c5-devkitc-1",
|
|
19
|
+
"esp32c6": "board-esp32-c6-devkitc-1",
|
|
20
|
+
"esp32h2": "board-esp32-h2-devkitm-1",
|
|
21
|
+
"esp32p4": "board-esp32-p4-function-ev",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_diagram(target: str) -> str:
|
|
26
|
+
"""Return a minimal diagram.json string for the given target."""
|
|
27
|
+
part_type = TARGET_WOKWI_PARTS.get(target, f"board-{target}-devkit")
|
|
28
|
+
diagram = {
|
|
29
|
+
"version": 1,
|
|
30
|
+
"author": "idf-wokwi",
|
|
31
|
+
"editor": "wokwi",
|
|
32
|
+
"parts": [{"type": part_type, "id": "esp"}],
|
|
33
|
+
"connections": [
|
|
34
|
+
["esp:TX", "$serialMonitor:RX", "", []],
|
|
35
|
+
["esp:RX", "$serialMonitor:TX", "", []],
|
|
36
|
+
],
|
|
37
|
+
"serialMonitor": {"display": "terminal"},
|
|
38
|
+
}
|
|
39
|
+
return json.dumps(diagram)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_diagram(project_path: str, diagram_file: str) -> str:
|
|
43
|
+
"""Locate diagram.json - returns path or empty string if auto-generation needed."""
|
|
44
|
+
if diagram_file:
|
|
45
|
+
if not os.path.isabs(diagram_file):
|
|
46
|
+
diagram_file = os.path.join(project_path, diagram_file)
|
|
47
|
+
if not os.path.exists(diagram_file):
|
|
48
|
+
red_print(f"Diagram file not found: {diagram_file}")
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
return diagram_file
|
|
51
|
+
|
|
52
|
+
default = os.path.join(project_path, "diagram.json")
|
|
53
|
+
if os.path.exists(default):
|
|
54
|
+
return default
|
|
55
|
+
|
|
56
|
+
return ""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
"""Line-based serial text/regex matcher"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from ._log import red_print, yellow_print
|
|
13
|
+
|
|
14
|
+
TIMEOUT_EXIT_CODE = 42
|
|
15
|
+
|
|
16
|
+
_MAX_LINE_LENGTH = 10_000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExpectEngine:
|
|
20
|
+
"""Buffer chars until newline, then test the complete line against patterns."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
expect_text: str = "",
|
|
25
|
+
fail_text: str = "",
|
|
26
|
+
expect_regex: str = "",
|
|
27
|
+
fail_regex: str = "",
|
|
28
|
+
) -> None:
|
|
29
|
+
self.expect_text = expect_text
|
|
30
|
+
self.fail_text = fail_text
|
|
31
|
+
self._expect_re = re.compile(expect_regex) if expect_regex else None
|
|
32
|
+
self._fail_re = re.compile(fail_regex) if fail_regex else None
|
|
33
|
+
self.match_event = threading.Event()
|
|
34
|
+
self.fail_event = threading.Event()
|
|
35
|
+
self.active = bool(expect_text or fail_text or self._expect_re or self._fail_re)
|
|
36
|
+
self._current_line: list[str] = []
|
|
37
|
+
|
|
38
|
+
def _test_line(self, line: str) -> None:
|
|
39
|
+
if self.expect_text and self.expect_text in line:
|
|
40
|
+
self.match_event.set()
|
|
41
|
+
if self._expect_re and self._expect_re.search(line):
|
|
42
|
+
self.match_event.set()
|
|
43
|
+
if self.fail_text and self.fail_text in line:
|
|
44
|
+
self.fail_event.set()
|
|
45
|
+
if self._fail_re and self._fail_re.search(line):
|
|
46
|
+
self.fail_event.set()
|
|
47
|
+
|
|
48
|
+
def feed(self, data: bytes) -> None:
|
|
49
|
+
"""Feed raw serial bytes - prints to stdout and tests for pattern matches."""
|
|
50
|
+
text = data.decode("utf-8", errors="replace")
|
|
51
|
+
print(text, end="", flush=True)
|
|
52
|
+
|
|
53
|
+
if not self.active:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
for ch in text:
|
|
57
|
+
if ch == "\n":
|
|
58
|
+
self._test_line("".join(self._current_line))
|
|
59
|
+
self._current_line.clear()
|
|
60
|
+
elif len(self._current_line) < _MAX_LINE_LENGTH:
|
|
61
|
+
self._current_line.append(ch)
|
|
62
|
+
|
|
63
|
+
def wait(self, timeout_ms: int) -> None:
|
|
64
|
+
"""Block until a match/fail is found or timeout elapses. Raises SystemExit."""
|
|
65
|
+
deadline = time.monotonic() + timeout_ms / 1000.0 if timeout_ms else float("inf")
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
remaining = deadline - time.monotonic()
|
|
69
|
+
if remaining <= 0:
|
|
70
|
+
red_print(f"Timeout after {timeout_ms} ms")
|
|
71
|
+
raise SystemExit(TIMEOUT_EXIT_CODE)
|
|
72
|
+
|
|
73
|
+
if self.fail_event.is_set():
|
|
74
|
+
msg = self.fail_text or str(self._fail_re and self._fail_re.pattern)
|
|
75
|
+
red_print(f"Fail text detected: {msg}")
|
|
76
|
+
raise SystemExit(1)
|
|
77
|
+
|
|
78
|
+
if self.match_event.is_set():
|
|
79
|
+
msg = self.expect_text or str(self._expect_re and self._expect_re.pattern)
|
|
80
|
+
yellow_print(f"Expected text found: {msg}")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
time.sleep(min(remaining, 0.1))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
"""Interactive mode - forward stdin to simulation, platform-aware."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from wokwi_client import WokwiClientSync
|
|
11
|
+
|
|
12
|
+
from ._log import yellow_print
|
|
13
|
+
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
import msvcrt
|
|
16
|
+
|
|
17
|
+
def _platform_loop(client: WokwiClientSync) -> None:
|
|
18
|
+
try:
|
|
19
|
+
while True:
|
|
20
|
+
ch = msvcrt.getch()
|
|
21
|
+
if ch == b"\x1d": # Ctrl+]
|
|
22
|
+
break
|
|
23
|
+
client.serial_write(ch)
|
|
24
|
+
except OSError:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
else:
|
|
28
|
+
import os
|
|
29
|
+
import termios
|
|
30
|
+
import tty
|
|
31
|
+
|
|
32
|
+
def _platform_loop(client: WokwiClientSync) -> None:
|
|
33
|
+
stdin_fd = sys.stdin.fileno()
|
|
34
|
+
old_settings = termios.tcgetattr(stdin_fd)
|
|
35
|
+
try:
|
|
36
|
+
tty.setcbreak(stdin_fd)
|
|
37
|
+
while True:
|
|
38
|
+
ch = os.read(stdin_fd, 1)
|
|
39
|
+
if ch == b"\x1d": # Ctrl+]
|
|
40
|
+
break
|
|
41
|
+
client.serial_write(ch)
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
finally:
|
|
45
|
+
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def interactive_loop(client: WokwiClientSync) -> None:
|
|
49
|
+
yellow_print("Press Ctrl+] to stop the simulation")
|
|
50
|
+
_platform_loop(client)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
ESP-IDF extension that adds ``idf.py wokwi`` for running Wokwi simulations.
|
|
6
|
+
|
|
7
|
+
Install once with ``pip install idf-wokwi`` and the command is available in
|
|
8
|
+
every ESP-IDF project.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from wokwi_client import GET_TOKEN_URL, WokwiClientSync
|
|
19
|
+
|
|
20
|
+
from ._log import red_print, yellow_print
|
|
21
|
+
from .diagram import find_diagram, generate_diagram
|
|
22
|
+
from .expect_engine import TIMEOUT_EXIT_CODE, ExpectEngine
|
|
23
|
+
from .interactive import interactive_loop
|
|
24
|
+
|
|
25
|
+
__all__ = ["TIMEOUT_EXIT_CODE", "action_extensions", "wokwi_callback"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_project_desc(build_dir: str) -> dict[str, Any]:
|
|
29
|
+
desc_path = os.path.join(build_dir, "project_description.json")
|
|
30
|
+
if not os.path.exists(desc_path):
|
|
31
|
+
red_print(f"Project description not found: {desc_path}")
|
|
32
|
+
red_print("Please build the project first with: idf.py build")
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
with open(desc_path, encoding="utf-8") as f:
|
|
35
|
+
project_desc: dict[str, Any] = json.load(f)
|
|
36
|
+
return project_desc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _upload_artifacts(
|
|
40
|
+
client: WokwiClientSync,
|
|
41
|
+
project_desc: dict[str, Any],
|
|
42
|
+
build_dir: str,
|
|
43
|
+
diagram_path: str,
|
|
44
|
+
flasher_args_path: str,
|
|
45
|
+
elf_path: str,
|
|
46
|
+
) -> tuple[str, str]:
|
|
47
|
+
"""Upload diagram, firmware, and ELF to the simulator. Returns (firmware_name, elf_name)."""
|
|
48
|
+
if diagram_path:
|
|
49
|
+
client.upload_file("diagram.json", Path(diagram_path))
|
|
50
|
+
yellow_print(f"Uploaded diagram: {diagram_path}")
|
|
51
|
+
else:
|
|
52
|
+
target: str = project_desc.get("target", "esp32")
|
|
53
|
+
diagram_content = generate_diagram(target)
|
|
54
|
+
client.upload("diagram.json", diagram_content.encode("utf-8"))
|
|
55
|
+
yellow_print(f"Generated diagram for target: {target}")
|
|
56
|
+
|
|
57
|
+
firmware_name: str = client.upload_file("flasher_args.json", Path(flasher_args_path))
|
|
58
|
+
yellow_print("Uploaded firmware")
|
|
59
|
+
|
|
60
|
+
elf_name = ""
|
|
61
|
+
if elf_path and os.path.exists(elf_path):
|
|
62
|
+
elf_name = client.upload_file(os.path.basename(elf_path), Path(elf_path))
|
|
63
|
+
yellow_print(f"Uploaded ELF: {os.path.basename(elf_path)}")
|
|
64
|
+
|
|
65
|
+
return firmware_name, elf_name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def wokwi_callback(
|
|
69
|
+
action: str,
|
|
70
|
+
ctx: Any,
|
|
71
|
+
args: Any,
|
|
72
|
+
diagram_file: str,
|
|
73
|
+
timeout: int,
|
|
74
|
+
expect_text: str,
|
|
75
|
+
fail_text: str,
|
|
76
|
+
expect_regex: str,
|
|
77
|
+
fail_regex: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
token = os.environ.get("WOKWI_CLI_TOKEN", "")
|
|
80
|
+
if not token:
|
|
81
|
+
red_print("WOKWI_CLI_TOKEN environment variable is not set.")
|
|
82
|
+
red_print(f"Get your token at: {GET_TOKEN_URL}")
|
|
83
|
+
raise SystemExit(1)
|
|
84
|
+
|
|
85
|
+
build_dir: str = args.build_dir
|
|
86
|
+
project_desc = _get_project_desc(build_dir)
|
|
87
|
+
project_path: str = project_desc.get("project_path", os.getcwd())
|
|
88
|
+
|
|
89
|
+
flasher_args_path = os.path.join(build_dir, "flasher_args.json")
|
|
90
|
+
if not os.path.exists(flasher_args_path):
|
|
91
|
+
red_print(f"flasher_args.json not found in {build_dir}")
|
|
92
|
+
red_print("Please build the project first with: idf.py build")
|
|
93
|
+
raise SystemExit(1)
|
|
94
|
+
|
|
95
|
+
elf_path = project_desc.get("app_elf", "")
|
|
96
|
+
if elf_path and not os.path.isabs(elf_path):
|
|
97
|
+
elf_path = os.path.join(build_dir, elf_path)
|
|
98
|
+
|
|
99
|
+
diagram_path = find_diagram(project_path, diagram_file)
|
|
100
|
+
|
|
101
|
+
server = os.environ.get("WOKWI_CLI_SERVER")
|
|
102
|
+
client = WokwiClientSync(token, server=server)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
hello = client.connect()
|
|
106
|
+
yellow_print(f"Connected to Wokwi Simulator (server {hello.get('version', '?')})")
|
|
107
|
+
|
|
108
|
+
firmware_name, elf_name = _upload_artifacts(
|
|
109
|
+
client, project_desc, build_dir, diagram_path, flasher_args_path, elf_path
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
engine = ExpectEngine(expect_text, fail_text, expect_regex, fail_regex)
|
|
113
|
+
client.serial_monitor(engine.feed)
|
|
114
|
+
|
|
115
|
+
client.start_simulation(firmware=firmware_name, elf=elf_name or None)
|
|
116
|
+
yellow_print("Simulation started")
|
|
117
|
+
|
|
118
|
+
if engine.active or timeout:
|
|
119
|
+
engine.wait(timeout)
|
|
120
|
+
else:
|
|
121
|
+
interactive_loop(client)
|
|
122
|
+
|
|
123
|
+
finally:
|
|
124
|
+
try:
|
|
125
|
+
client.disconnect()
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def action_extensions(base_actions: dict[str, Any], project_path: str) -> dict[str, Any]:
|
|
131
|
+
return {
|
|
132
|
+
"version": "1",
|
|
133
|
+
"actions": {
|
|
134
|
+
"wokwi": {
|
|
135
|
+
"callback": wokwi_callback,
|
|
136
|
+
"help": "Run Wokwi simulation.",
|
|
137
|
+
"dependencies": ["all"],
|
|
138
|
+
"options": [
|
|
139
|
+
{
|
|
140
|
+
"names": ["--diagram-file"],
|
|
141
|
+
"help": (
|
|
142
|
+
"Path to diagram.json file. "
|
|
143
|
+
"If not specified, looks for diagram.json in the project root."
|
|
144
|
+
),
|
|
145
|
+
"is_flag": False,
|
|
146
|
+
"default": "",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"names": ["--timeout"],
|
|
150
|
+
"help": (
|
|
151
|
+
"Simulation timeout in milliseconds. "
|
|
152
|
+
"Exits with code 42 on timeout. 0 means no timeout."
|
|
153
|
+
),
|
|
154
|
+
"is_flag": False,
|
|
155
|
+
"default": 0,
|
|
156
|
+
"type": int,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"names": ["--expect-text"],
|
|
160
|
+
"help": "Exit successfully when this text appears in serial output.",
|
|
161
|
+
"is_flag": False,
|
|
162
|
+
"default": "",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"names": ["--fail-text"],
|
|
166
|
+
"help": "Exit with error when this text appears in serial output.",
|
|
167
|
+
"is_flag": False,
|
|
168
|
+
"default": "",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"names": ["--expect-regex"],
|
|
172
|
+
"help": "Exit successfully when this regex matches a serial output line.",
|
|
173
|
+
"is_flag": False,
|
|
174
|
+
"default": "",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"names": ["--fail-regex"],
|
|
178
|
+
"help": "Exit with error when this regex matches a serial output line.",
|
|
179
|
+
"is_flag": False,
|
|
180
|
+
"default": "",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include "freertos/FreeRTOS.h"
|
|
3
|
+
#include "freertos/task.h"
|
|
4
|
+
|
|
5
|
+
void app_main(void)
|
|
6
|
+
{
|
|
7
|
+
printf("Hello world!\n");
|
|
8
|
+
for (int i = 10; i >= 0; i--) {
|
|
9
|
+
printf("Restarting in %d seconds...\n", i);
|
|
10
|
+
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
|
11
|
+
}
|
|
12
|
+
esp_restart();
|
|
13
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2026, CodeMagic LTD
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any
|
|
9
|
+
from unittest.mock import MagicMock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from idf_wokwi.wokwi_ext import TIMEOUT_EXIT_CODE, action_extensions, wokwi_callback
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _hello_world_build_dir() -> str:
|
|
17
|
+
"""Return path to a pre-built hello_world build directory.
|
|
18
|
+
|
|
19
|
+
Set BUILD_DIR env var to override.
|
|
20
|
+
"""
|
|
21
|
+
explicit = os.environ.get("BUILD_DIR")
|
|
22
|
+
if explicit:
|
|
23
|
+
assert os.path.isfile(os.path.join(explicit, "flasher_args.json")), (
|
|
24
|
+
f"BUILD_DIR={explicit} does not contain flasher_args.json"
|
|
25
|
+
)
|
|
26
|
+
return explicit
|
|
27
|
+
|
|
28
|
+
idf_path = os.environ.get("IDF_PATH", "/workspace/esp-idf")
|
|
29
|
+
candidates = [
|
|
30
|
+
os.path.join(idf_path, "examples", "get-started", "hello_world", "build"),
|
|
31
|
+
]
|
|
32
|
+
for c in candidates:
|
|
33
|
+
if os.path.isfile(os.path.join(c, "flasher_args.json")):
|
|
34
|
+
return c
|
|
35
|
+
pytest.fail("No hello_world build directory with flasher_args.json found")
|
|
36
|
+
raise AssertionError # unreachable, helps mypy
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_action_extensions_structure() -> None:
|
|
40
|
+
result = action_extensions({}, "/tmp")
|
|
41
|
+
assert result["version"] == "1"
|
|
42
|
+
assert "wokwi" in result["actions"]
|
|
43
|
+
assert callable(result["actions"]["wokwi"]["callback"])
|
|
44
|
+
assert result["actions"]["wokwi"]["dependencies"] == ["all"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestWokwiIntegration:
|
|
48
|
+
"""Integration tests that connect to a real Wokwi CI server."""
|
|
49
|
+
|
|
50
|
+
def _make_args(self, build_dir: str) -> Any:
|
|
51
|
+
mock = MagicMock()
|
|
52
|
+
mock.build_dir = build_dir
|
|
53
|
+
return mock
|
|
54
|
+
|
|
55
|
+
def _run_callback_in_thread(
|
|
56
|
+
self,
|
|
57
|
+
build_dir: str,
|
|
58
|
+
*,
|
|
59
|
+
timeout: int = 10000,
|
|
60
|
+
expect_text: str = "",
|
|
61
|
+
fail_text: str = "",
|
|
62
|
+
expect_regex: str = "",
|
|
63
|
+
fail_regex: str = "",
|
|
64
|
+
diagram_file: str = "",
|
|
65
|
+
) -> int:
|
|
66
|
+
"""Run wokwi_callback in a thread and return the exit code (0=success)."""
|
|
67
|
+
exit_code = 0
|
|
68
|
+
|
|
69
|
+
def _target() -> None:
|
|
70
|
+
nonlocal exit_code
|
|
71
|
+
try:
|
|
72
|
+
wokwi_callback(
|
|
73
|
+
action="wokwi",
|
|
74
|
+
ctx=None,
|
|
75
|
+
args=self._make_args(build_dir),
|
|
76
|
+
diagram_file=diagram_file,
|
|
77
|
+
timeout=timeout,
|
|
78
|
+
expect_text=expect_text,
|
|
79
|
+
fail_text=fail_text,
|
|
80
|
+
expect_regex=expect_regex,
|
|
81
|
+
fail_regex=fail_regex,
|
|
82
|
+
)
|
|
83
|
+
except SystemExit as e:
|
|
84
|
+
exit_code = e.code if isinstance(e.code, int) else 1
|
|
85
|
+
|
|
86
|
+
t = threading.Thread(target=_target)
|
|
87
|
+
t.start()
|
|
88
|
+
t.join(timeout=30)
|
|
89
|
+
if t.is_alive():
|
|
90
|
+
exit_code = -1
|
|
91
|
+
return exit_code
|
|
92
|
+
|
|
93
|
+
def test_expect_text_hello_world(self) -> None:
|
|
94
|
+
"""Simulation should find 'Hello world' in serial output."""
|
|
95
|
+
build_dir = _hello_world_build_dir()
|
|
96
|
+
code = self._run_callback_in_thread(build_dir, timeout=15000, expect_text="Hello world")
|
|
97
|
+
assert code == 0
|
|
98
|
+
|
|
99
|
+
def test_timeout_returns_42(self) -> None:
|
|
100
|
+
"""A very short timeout should exit with code 42."""
|
|
101
|
+
build_dir = _hello_world_build_dir()
|
|
102
|
+
code = self._run_callback_in_thread(
|
|
103
|
+
build_dir, timeout=1, expect_text="this_text_will_never_appear"
|
|
104
|
+
)
|
|
105
|
+
assert code == TIMEOUT_EXIT_CODE
|
|
106
|
+
|
|
107
|
+
def test_fail_text(self) -> None:
|
|
108
|
+
"""fail-text should cause exit code 1."""
|
|
109
|
+
build_dir = _hello_world_build_dir()
|
|
110
|
+
# "rst:0x1" appears early in ESP32 boot output
|
|
111
|
+
code = self._run_callback_in_thread(build_dir, timeout=15000, fail_text="rst:0x1")
|
|
112
|
+
assert code == 1
|
|
113
|
+
|
|
114
|
+
def test_expect_regex(self) -> None:
|
|
115
|
+
"""expect-regex should match serial output with a pattern."""
|
|
116
|
+
build_dir = _hello_world_build_dir()
|
|
117
|
+
code = self._run_callback_in_thread(build_dir, timeout=15000, expect_regex=r"Hello\s+world")
|
|
118
|
+
assert code == 0
|
|
119
|
+
|
|
120
|
+
def test_fail_regex(self) -> None:
|
|
121
|
+
"""fail-regex should cause exit code 1 on pattern match."""
|
|
122
|
+
build_dir = _hello_world_build_dir()
|
|
123
|
+
code = self._run_callback_in_thread(build_dir, timeout=15000, fail_regex=r"rst:0x[0-9a-f]+")
|
|
124
|
+
assert code == 1
|