pytest-embedded-wireskein 0.0.1__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.
- pytest_embedded_wireskein-0.0.1/.gitignore +36 -0
- pytest_embedded_wireskein-0.0.1/CHANGELOG.md +7 -0
- pytest_embedded_wireskein-0.0.1/LICENSE +21 -0
- pytest_embedded_wireskein-0.0.1/PKG-INFO +111 -0
- pytest_embedded_wireskein-0.0.1/README.ja.md +101 -0
- pytest_embedded_wireskein-0.0.1/README.md +92 -0
- pytest_embedded_wireskein-0.0.1/pyproject.toml +48 -0
- pytest_embedded_wireskein-0.0.1/src/pytest_embedded_wireskein/__init__.py +5 -0
- pytest_embedded_wireskein-0.0.1/src/pytest_embedded_wireskein/plugin.py +91 -0
- pytest_embedded_wireskein-0.0.1/tests/test_plugin.py +86 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
|
|
7
|
+
# Build / packaging
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.eggs/
|
|
12
|
+
pip-wheel-metadata/
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
env/
|
|
18
|
+
.env
|
|
19
|
+
|
|
20
|
+
# Test / tooling caches
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
.coverage.*
|
|
24
|
+
htmlcov/
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
.ruff_cache/
|
|
27
|
+
report.html
|
|
28
|
+
|
|
29
|
+
# IDE / OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
|
|
34
|
+
# Local dependency locks
|
|
35
|
+
uv.lock
|
|
36
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog / 変更履歴
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.0.1
|
|
6
|
+
- (EN) First beta. The `ws_run` fixture gives each test a `wireskein.runlog.Recorder` writing into `<test_case_tempdir>/wireskein/`, next to `dut.log`. When the test body ends the run is verified: a failing check fails the test in its call phase (FAILED), `report.json` and `report.xml` are written, and the result lines are attached to the test report. `--wireskein-verify` / `wireskein_verify` = `fail` (default), `report` or `off`.
|
|
7
|
+
- (JA) 最初のβ版。`ws_run` fixture が test ごとに `wireskein.runlog.Recorder` を渡し、`dut.log` の隣の `<test_case_tempdir>/wireskein/` に記録する。test の本体が終わると照合する。検査が NG なら call の段で test を失敗(FAILED)にし、`report.json` と `report.xml` を書き、結果の行を test の報告に付ける。`--wireskein-verify` / `wireskein_verify` は `fail`(既定)、`report`、`off`。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Embedded Probe
|
|
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.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytest-embedded-wireskein
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: pytest-embedded plugin: record logic-analyzer captures per test with WireSkein and fail the test when a waveform check fails
|
|
5
|
+
Project-URL: Homepage, https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein
|
|
6
|
+
Project-URL: Repository, https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein
|
|
7
|
+
Author: TANAKA Masayuki
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: pytest-embedded>=2.0
|
|
16
|
+
Requires-Dist: pytest>=8
|
|
17
|
+
Requires-Dist: wireskein>=0.0.1
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# pytest-embedded-wireskein
|
|
21
|
+
|
|
22
|
+
[日本語 README](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein/blob/main/README.ja.md)
|
|
23
|
+
|
|
24
|
+
A [pytest-embedded](https://github.com/espressif/pytest-embedded) plugin that gives each test a [WireSkein](https://github.com/Open-Embedded-Probe/wireskein) recorder, `ws_run`. The test records the commands it sent, the logic-analyzer captures, and what each step should look like on the wire. When the test body ends, the plugin checks the captures against these expectations and fails the test if a check fails.
|
|
25
|
+
|
|
26
|
+
Status: **beta**.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install pytest-embedded-wireskein
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Python 3.13 or newer. This installs `wireskein` and `pytest-embedded`.
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from wireskein.runlog import square, level, only_moving
|
|
40
|
+
|
|
41
|
+
def test_pwm(dut, ws_run, probe): # `probe` is whatever drives your logic analyzer
|
|
42
|
+
with ws_run.section(1, "pwm"):
|
|
43
|
+
for duty in (64, 128, 0):
|
|
44
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
45
|
+
with ws_run.section(2, f"duty={duty}", expect=want):
|
|
46
|
+
ws_run.command(f"PWM {duty}")
|
|
47
|
+
dut.write(f"PWM {duty}")
|
|
48
|
+
ws_run.reply(dut.expect(r"PWM duty=\d+").group(0).decode())
|
|
49
|
+
t = ws_run.armed() # right after arming the capture
|
|
50
|
+
data, rate = probe.capture() # bytes, one sample per byte, bit k = pin k
|
|
51
|
+
ws_run.capture(data, rate, ["PA1", "PA0"], t)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When `test_pwm` returns, the plugin closes the run and verifies it. If a check fails, the test fails in its call phase (FAILED, not ERROR):
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
FAILED test_pwm.py::test_pwm - wireskein: 1 NG (5 ok, 1 ng, 0 unchecked (4 segments, 3 captures))
|
|
58
|
+
NG pwm/duty=128 square c0002.bin duty 0.6999 vs 0.5020
|
|
59
|
+
report: /tmp/pytest-embedded/2026-09-29_12-00-00-000000/test_pwm/wireskein/report.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The checks (`square`, `level`, `starts`, `ends`, `only_moving`, `pulses`, `i2c`, `spi`, `uart`) and the heading rules are described in the [WireSkein README](https://github.com/Open-Embedded-Probe/wireskein#checking-a-test-run).
|
|
63
|
+
|
|
64
|
+
### Where the run is written
|
|
65
|
+
|
|
66
|
+
`ws_run` writes to `<test_case_tempdir>/wireskein/`, next to pytest-embedded's `dut.log`: `<root-logdir>/pytest-embedded/<time>/<test name>/wireskein/`.
|
|
67
|
+
|
|
68
|
+
| File | Content |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `run.json` | Headings, commands, replies, notes, captures and expectations (WireSkein run format) |
|
|
71
|
+
| `c0001.bin`, ... | The captures |
|
|
72
|
+
| `report.json` | Every result with measured values, and the log |
|
|
73
|
+
| `report.xml` | The results as JUnit XML |
|
|
74
|
+
|
|
75
|
+
The same directory can be checked again later with `wireskein verify <dir>`. The test's `user_properties` carry `wireskein_report` (the path of `report.json`), and the result lines are added to the test report as a `wireskein` section (shown with `-rA` or on failure).
|
|
76
|
+
|
|
77
|
+
### When the plugin verifies
|
|
78
|
+
|
|
79
|
+
- The run is verified after the test body, only if the test recorded a capture or an expectation.
|
|
80
|
+
- A check that fails makes the test fail.
|
|
81
|
+
- A check whose pins were not captured is unchecked and does not fail the test.
|
|
82
|
+
- If the test body already failed, the run is still recorded and verified, but the test's own failure is kept.
|
|
83
|
+
|
|
84
|
+
### Options
|
|
85
|
+
|
|
86
|
+
| Option | ini | Default | Meaning |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `--wireskein-verify=fail\|report\|off` | `wireskein_verify` | `fail` | `fail`: verify and fail on NG. `report`: verify and write the reports, never fail. `off`: record only |
|
|
89
|
+
|
|
90
|
+
### Connecting a probe
|
|
91
|
+
|
|
92
|
+
This plugin knows nothing about probes or targets. A fixture that drives the logic analyzer (for example the one of a board-family plugin) connects to `ws_run` when both are installed:
|
|
93
|
+
|
|
94
|
+
- right after arming a capture: `t = ws_run.armed()`
|
|
95
|
+
- when the samples are read: `ws_run.capture(data, rate, bits, t, start_us=..., time_base_slipped=True)`. `bits` lists the target's pin names in bit order. Pass `time_base_slipped` only when the probe reports it.
|
|
96
|
+
- the console traffic: `ws_run.command(text)`, `ws_run.reply(text)`
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv sync
|
|
102
|
+
uv run pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Release
|
|
106
|
+
|
|
107
|
+
The release works the same way as in [pytest-embedded-arduino-cli](https://github.com/tanakamasayuki/pytest-embedded-arduino-cli#release). Update `## Unreleased` in `CHANGELOG.md`, then run the `Release` workflow with the version (for example `0.0.2`). PyPI publishing uses Trusted Publishing.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pytest-embedded-wireskein
|
|
2
|
+
|
|
3
|
+
[English README](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
[pytest-embedded](https://github.com/espressif/pytest-embedded) のプラグインです。test ごとに、[WireSkein](https://github.com/Open-Embedded-Probe/wireskein) の記録器 `ws_run` を渡します。
|
|
6
|
+
|
|
7
|
+
- test は、送ったコマンド、ロジックアナライザのキャプチャ、各ステップの線の上であるべき姿を記録します。
|
|
8
|
+
- test の本体が終わると、プラグインがキャプチャを期待と照らし合わせます。
|
|
9
|
+
- 検査が NG なら、その test を失敗にします。
|
|
10
|
+
|
|
11
|
+
状態: **β 版**です。
|
|
12
|
+
|
|
13
|
+
## 入れ方
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install pytest-embedded-wireskein
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.13 以上が要ります。`wireskein` と `pytest-embedded` も一緒に入ります。
|
|
20
|
+
|
|
21
|
+
## 使い方
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from wireskein.runlog import square, level, only_moving
|
|
25
|
+
|
|
26
|
+
def test_pwm(dut, ws_run, probe): # probe は、ロジックアナライザを動かす何かの fixture
|
|
27
|
+
with ws_run.section(1, "pwm"):
|
|
28
|
+
for duty in (64, 128, 0):
|
|
29
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
30
|
+
with ws_run.section(2, f"duty={duty}", expect=want):
|
|
31
|
+
ws_run.command(f"PWM {duty}")
|
|
32
|
+
dut.write(f"PWM {duty}")
|
|
33
|
+
ws_run.reply(dut.expect(r"PWM duty=\d+").group(0).decode())
|
|
34
|
+
t = ws_run.armed() # キャプチャを開始した直後
|
|
35
|
+
data, rate = probe.capture() # bytes。1 サンプル 1 バイト、ビット k がピン k
|
|
36
|
+
ws_run.capture(data, rate, ["PA1", "PA0"], t)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`test_pwm` が戻ると、プラグインは記録を閉じて照合します。NG があれば、test は call の段で失敗します(ERROR ではなく FAILED)。
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
FAILED test_pwm.py::test_pwm - wireskein: 1 NG (5 ok, 1 ng, 0 unchecked (4 segments, 3 captures))
|
|
43
|
+
NG pwm/duty=128 square c0002.bin duty 0.6999 vs 0.5020
|
|
44
|
+
report: /tmp/pytest-embedded/2026-09-29_12-00-00-000000/test_pwm/wireskein/report.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
検査の一覧(`square`、`level`、`starts`、`ends`、`only_moving`、`pulses`、`i2c`、`spi`、`uart`)と見出しの規則は、[WireSkein の README](https://github.com/Open-Embedded-Probe/wireskein/blob/main/README.ja.md) にあります。
|
|
48
|
+
|
|
49
|
+
### 記録の置き場所
|
|
50
|
+
|
|
51
|
+
`ws_run` は、pytest-embedded の `dut.log` の隣の `<test_case_tempdir>/wireskein/` に書きます。フルパスは `<root-logdir>/pytest-embedded/<時刻>/<test 名>/wireskein/` です。
|
|
52
|
+
|
|
53
|
+
| ファイル | 中身 |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `run.json` | 見出し、コマンド、応答、メモ、キャプチャ、期待(WireSkein の記録の形式) |
|
|
56
|
+
| `c0001.bin` など | キャプチャ |
|
|
57
|
+
| `report.json` | すべての結果と測定値、ログ |
|
|
58
|
+
| `report.xml` | 結果の JUnit XML |
|
|
59
|
+
|
|
60
|
+
同じディレクトリは、後から `wireskein verify <ディレクトリ>` で照合し直せます。
|
|
61
|
+
|
|
62
|
+
- test の `user_properties` には、`wireskein_report`(`report.json` のパス)が入ります。
|
|
63
|
+
- 結果の行は、test の報告に `wireskein` の節として付きます(`-rA` や失敗のときに表示されます)。
|
|
64
|
+
|
|
65
|
+
### 照合する条件
|
|
66
|
+
|
|
67
|
+
- test が、キャプチャか期待を 1 つでも記録したときだけ照合します。
|
|
68
|
+
- NG の検査があれば、test を失敗にします。
|
|
69
|
+
- ピンがキャプチャにない検査は未検査で、失敗にはしません。
|
|
70
|
+
- test の本体がすでに失敗していた場合も、記録と照合は行います。ただし、test 自身の失敗の内容はそのまま残します。
|
|
71
|
+
|
|
72
|
+
### 設定
|
|
73
|
+
|
|
74
|
+
| オプション | ini | 既定 | 意味 |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `--wireskein-verify=fail\|report\|off` | `wireskein_verify` | `fail` | `fail`: 照合して、NG なら失敗。`report`: 照合して報告だけ残す(失敗にしない)。`off`: 記録だけ |
|
|
77
|
+
|
|
78
|
+
### プローブとのつなぎ方
|
|
79
|
+
|
|
80
|
+
このプラグインは、プローブもターゲットも知りません。ロジックアナライザを動かす fixture(例: ボードの家系ごとのプラグイン)が、両方入っているときに `ws_run` へつなぎます。
|
|
81
|
+
|
|
82
|
+
- キャプチャを開始した直後に `t = ws_run.armed()`
|
|
83
|
+
- 読み終えたら `ws_run.capture(data, rate, bits, t, start_us=..., time_base_slipped=True)`
|
|
84
|
+
- `bits` は、ビットの順に並べたターゲットのピン名です。
|
|
85
|
+
- `time_base_slipped` は、プローブが報告したときだけ渡します。
|
|
86
|
+
- コンソールの送受信は `ws_run.command(text)` と `ws_run.reply(text)`
|
|
87
|
+
|
|
88
|
+
## 開発
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
uv sync
|
|
92
|
+
uv run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## リリース
|
|
96
|
+
|
|
97
|
+
[pytest-embedded-arduino-cli](https://github.com/tanakamasayuki/pytest-embedded-arduino-cli/blob/main/README.ja.md) と同じ手順です。`CHANGELOG.md` の `## Unreleased` を更新し、`Release` の workflow を版(例 `0.0.2`)を入れて実行します。PyPI への公開は Trusted Publishing で行います。
|
|
98
|
+
|
|
99
|
+
## ライセンス
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# pytest-embedded-wireskein
|
|
2
|
+
|
|
3
|
+
[日本語 README](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein/blob/main/README.ja.md)
|
|
4
|
+
|
|
5
|
+
A [pytest-embedded](https://github.com/espressif/pytest-embedded) plugin that gives each test a [WireSkein](https://github.com/Open-Embedded-Probe/wireskein) recorder, `ws_run`. The test records the commands it sent, the logic-analyzer captures, and what each step should look like on the wire. When the test body ends, the plugin checks the captures against these expectations and fails the test if a check fails.
|
|
6
|
+
|
|
7
|
+
Status: **beta**.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install pytest-embedded-wireskein
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Python 3.13 or newer. This installs `wireskein` and `pytest-embedded`.
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from wireskein.runlog import square, level, only_moving
|
|
21
|
+
|
|
22
|
+
def test_pwm(dut, ws_run, probe): # `probe` is whatever drives your logic analyzer
|
|
23
|
+
with ws_run.section(1, "pwm"):
|
|
24
|
+
for duty in (64, 128, 0):
|
|
25
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
26
|
+
with ws_run.section(2, f"duty={duty}", expect=want):
|
|
27
|
+
ws_run.command(f"PWM {duty}")
|
|
28
|
+
dut.write(f"PWM {duty}")
|
|
29
|
+
ws_run.reply(dut.expect(r"PWM duty=\d+").group(0).decode())
|
|
30
|
+
t = ws_run.armed() # right after arming the capture
|
|
31
|
+
data, rate = probe.capture() # bytes, one sample per byte, bit k = pin k
|
|
32
|
+
ws_run.capture(data, rate, ["PA1", "PA0"], t)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
When `test_pwm` returns, the plugin closes the run and verifies it. If a check fails, the test fails in its call phase (FAILED, not ERROR):
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
FAILED test_pwm.py::test_pwm - wireskein: 1 NG (5 ok, 1 ng, 0 unchecked (4 segments, 3 captures))
|
|
39
|
+
NG pwm/duty=128 square c0002.bin duty 0.6999 vs 0.5020
|
|
40
|
+
report: /tmp/pytest-embedded/2026-09-29_12-00-00-000000/test_pwm/wireskein/report.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The checks (`square`, `level`, `starts`, `ends`, `only_moving`, `pulses`, `i2c`, `spi`, `uart`) and the heading rules are described in the [WireSkein README](https://github.com/Open-Embedded-Probe/wireskein#checking-a-test-run).
|
|
44
|
+
|
|
45
|
+
### Where the run is written
|
|
46
|
+
|
|
47
|
+
`ws_run` writes to `<test_case_tempdir>/wireskein/`, next to pytest-embedded's `dut.log`: `<root-logdir>/pytest-embedded/<time>/<test name>/wireskein/`.
|
|
48
|
+
|
|
49
|
+
| File | Content |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `run.json` | Headings, commands, replies, notes, captures and expectations (WireSkein run format) |
|
|
52
|
+
| `c0001.bin`, ... | The captures |
|
|
53
|
+
| `report.json` | Every result with measured values, and the log |
|
|
54
|
+
| `report.xml` | The results as JUnit XML |
|
|
55
|
+
|
|
56
|
+
The same directory can be checked again later with `wireskein verify <dir>`. The test's `user_properties` carry `wireskein_report` (the path of `report.json`), and the result lines are added to the test report as a `wireskein` section (shown with `-rA` or on failure).
|
|
57
|
+
|
|
58
|
+
### When the plugin verifies
|
|
59
|
+
|
|
60
|
+
- The run is verified after the test body, only if the test recorded a capture or an expectation.
|
|
61
|
+
- A check that fails makes the test fail.
|
|
62
|
+
- A check whose pins were not captured is unchecked and does not fail the test.
|
|
63
|
+
- If the test body already failed, the run is still recorded and verified, but the test's own failure is kept.
|
|
64
|
+
|
|
65
|
+
### Options
|
|
66
|
+
|
|
67
|
+
| Option | ini | Default | Meaning |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `--wireskein-verify=fail\|report\|off` | `wireskein_verify` | `fail` | `fail`: verify and fail on NG. `report`: verify and write the reports, never fail. `off`: record only |
|
|
70
|
+
|
|
71
|
+
### Connecting a probe
|
|
72
|
+
|
|
73
|
+
This plugin knows nothing about probes or targets. A fixture that drives the logic analyzer (for example the one of a board-family plugin) connects to `ws_run` when both are installed:
|
|
74
|
+
|
|
75
|
+
- right after arming a capture: `t = ws_run.armed()`
|
|
76
|
+
- when the samples are read: `ws_run.capture(data, rate, bits, t, start_us=..., time_base_slipped=True)`. `bits` lists the target's pin names in bit order. Pass `time_base_slipped` only when the probe reports it.
|
|
77
|
+
- the console traffic: `ws_run.command(text)`, `ws_run.reply(text)`
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
uv sync
|
|
83
|
+
uv run pytest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Release
|
|
87
|
+
|
|
88
|
+
The release works the same way as in [pytest-embedded-arduino-cli](https://github.com/tanakamasayuki/pytest-embedded-arduino-cli#release). Update `## Unreleased` in `CHANGELOG.md`, then run the `Release` workflow with the version (for example `0.0.2`). PyPI publishing uses Trusted Publishing.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-embedded-wireskein"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "pytest-embedded plugin: record logic-analyzer captures per test with WireSkein and fail the test when a waveform check fails"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TANAKA Masayuki" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Framework :: Pytest",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"pytest-embedded>=2.0",
|
|
24
|
+
"wireskein>=0.0.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.entry-points.pytest11]
|
|
28
|
+
embedded-wireskein = "pytest_embedded_wireskein.plugin"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein"
|
|
32
|
+
Repository = "https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/pytest_embedded_wireskein"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
only-include = ["src/pytest_embedded_wireskein", "tests", "README.md", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
addopts = "-p pytester"
|
|
43
|
+
filterwarnings = [
|
|
44
|
+
"ignore:record_xml_attribute is an experimental feature:pytest.PytestExperimentalApiWarning",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""`ws_run`: a WireSkein recorder per test, verified when the test body ends.
|
|
2
|
+
|
|
3
|
+
The run is written next to pytest-embedded's dut.log, in
|
|
4
|
+
<test_case_tempdir>/wireskein/ (run.json, the captures, report.json,
|
|
5
|
+
report.xml). A failing check fails the test in its call phase, so pytest
|
|
6
|
+
reports FAILED, not ERROR. The plugin knows nothing about the probe or the
|
|
7
|
+
target: whatever fixture drives the probe feeds `ws_run` (armed / capture /
|
|
8
|
+
command / reply).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from wireskein import verify as ws_verify
|
|
17
|
+
from wireskein.runlog import Recorder
|
|
18
|
+
|
|
19
|
+
MODES = ("fail", "report", "off")
|
|
20
|
+
DIR_NAME = "wireskein"
|
|
21
|
+
|
|
22
|
+
_recorder_key = pytest.StashKey[Recorder]()
|
|
23
|
+
_done_key = pytest.StashKey[bool]()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
27
|
+
group = parser.getgroup("embedded-wireskein")
|
|
28
|
+
group.addoption(
|
|
29
|
+
"--wireskein-verify",
|
|
30
|
+
choices=MODES,
|
|
31
|
+
default=None,
|
|
32
|
+
help="after each test using ws_run: fail = verify and fail the test on NG (default), "
|
|
33
|
+
"report = verify and keep the reports only, off = record only",
|
|
34
|
+
)
|
|
35
|
+
parser.addini("wireskein_verify", help="fail | report | off (see --wireskein-verify)", default="fail")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
39
|
+
_mode(config) # a wrong value is a usage error before any test runs
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _mode(config: pytest.Config) -> str:
|
|
43
|
+
mode = config.getoption("wireskein_verify") or config.getini("wireskein_verify")
|
|
44
|
+
if mode not in MODES:
|
|
45
|
+
raise pytest.UsageError(f"wireskein_verify must be one of {', '.join(MODES)}, not {mode!r}")
|
|
46
|
+
return mode
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def ws_run(request: pytest.FixtureRequest, test_case_tempdir: str) -> Recorder:
|
|
51
|
+
"""A wireskein.runlog.Recorder for this test, recording into
|
|
52
|
+
<test_case_tempdir>/wireskein/. Headings (`section`), commands, replies,
|
|
53
|
+
captures and expectations go through it; the plugin verifies the run when
|
|
54
|
+
the test body ends."""
|
|
55
|
+
rec = Recorder(Path(test_case_tempdir) / DIR_NAME, test=request.node.nodeid)
|
|
56
|
+
request.node.stash[_recorder_key] = rec
|
|
57
|
+
yield rec
|
|
58
|
+
# the call phase did not run (a setup error): still leave the record
|
|
59
|
+
_finish(request.node, verdict=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.hookimpl(wrapper=True)
|
|
63
|
+
def pytest_runtest_call(item: pytest.Item):
|
|
64
|
+
try:
|
|
65
|
+
result = yield
|
|
66
|
+
except BaseException:
|
|
67
|
+
_finish(item, verdict=False) # the test failed on its own: record, do not override
|
|
68
|
+
raise
|
|
69
|
+
_finish(item, verdict=True)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _finish(item: pytest.Item, verdict: bool) -> None:
|
|
74
|
+
rec = item.stash.get(_recorder_key, None)
|
|
75
|
+
if rec is None or item.stash.get(_done_key, False):
|
|
76
|
+
return
|
|
77
|
+
item.stash[_done_key] = True
|
|
78
|
+
path = rec.close()
|
|
79
|
+
mode = _mode(item.config)
|
|
80
|
+
if mode == "off" or not (rec.doc["expect"] or rec.doc["captures"]):
|
|
81
|
+
return
|
|
82
|
+
report = ws_verify.verify(path.parent)
|
|
83
|
+
(path.parent / "report.json").write_text(ws_verify.dumps(report))
|
|
84
|
+
(path.parent / "report.xml").write_text(ws_verify.junit(report))
|
|
85
|
+
item.user_properties.append(("wireskein_report", str(path.parent / "report.json")))
|
|
86
|
+
text = "\n".join([ws_verify.summary_line(report), *ws_verify.lines(report), f"report: {path.parent / 'report.json'}"])
|
|
87
|
+
item.add_report_section("call", "wireskein", text)
|
|
88
|
+
if verdict and mode == "fail" and report["summary"]["ng"]:
|
|
89
|
+
ng = [line for line in ws_verify.lines(report) if line.startswith("NG")]
|
|
90
|
+
pytest.fail(f"wireskein: {report['summary']['ng']} NG ({ws_verify.summary_line(report)})\n"
|
|
91
|
+
+ "\n".join(ng) + f"\nreport: {path.parent / 'report.json'}", pytrace=False)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
# A test file using ws_run with a synthetic capture: P0 low for 1000 samples.
|
|
7
|
+
TEST_FILE = '''
|
|
8
|
+
from wireskein.runlog import level
|
|
9
|
+
|
|
10
|
+
def test_{name}(ws_run):
|
|
11
|
+
with ws_run.section(1, "t", expect=[level("P0", {want})]):
|
|
12
|
+
ws_run.command("PING")
|
|
13
|
+
ws_run.reply("PONG")
|
|
14
|
+
t = ws_run.armed()
|
|
15
|
+
ws_run.capture(bytes(1000), 1e6, ["P0"], t)
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(pytester: pytest.Pytester, *args, name="x", want=0, body=None):
|
|
20
|
+
pytester.makepyfile(body or TEST_FILE.format(name=name, want=want))
|
|
21
|
+
logdir = pytester.path / "logs"
|
|
22
|
+
return pytester.runpytest("--root-logdir", str(logdir), *args), logdir
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_dir(logdir: Path, name: str) -> Path:
|
|
26
|
+
(d,) = logdir.glob(f"pytest-embedded/*/test_{name}/wireskein")
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_ok_run_is_recorded_next_to_the_dut_log(pytester):
|
|
31
|
+
result, logdir = run(pytester)
|
|
32
|
+
result.assert_outcomes(passed=1)
|
|
33
|
+
d = run_dir(logdir, "x")
|
|
34
|
+
assert {p.name for p in d.iterdir()} == {"run.json", "c0001.bin", "report.json", "report.xml"}
|
|
35
|
+
doc = json.loads((d / "run.json").read_text())
|
|
36
|
+
assert doc["meta"]["test"].endswith("::test_x")
|
|
37
|
+
assert [e["src"] for e in doc["log"]] == ["marker", "host", "dut", "marker"]
|
|
38
|
+
assert json.loads((d / "report.json").read_text())["summary"]["ok"] == 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_ng_fails_the_call_phase(pytester):
|
|
42
|
+
result, logdir = run(pytester, want=1)
|
|
43
|
+
result.assert_outcomes(failed=1) # FAILED, not ERROR
|
|
44
|
+
result.stdout.fnmatch_lines(["*wireskein: 1 NG*", "*NG t level c0001.bin not constant 1*", "*report: *report.json*"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_report_mode_keeps_the_reports_without_failing(pytester):
|
|
48
|
+
result, logdir = run(pytester, "--wireskein-verify=report", want=1)
|
|
49
|
+
result.assert_outcomes(passed=1)
|
|
50
|
+
assert json.loads((run_dir(logdir, "x") / "report.json").read_text())["summary"]["ng"] == 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_off_mode_only_records(pytester):
|
|
54
|
+
pytester.makeini("[pytest]\nwireskein_verify = off\n")
|
|
55
|
+
result, logdir = run(pytester, want=1)
|
|
56
|
+
result.assert_outcomes(passed=1)
|
|
57
|
+
assert {p.name for p in run_dir(logdir, "x").iterdir()} == {"run.json", "c0001.bin"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_a_failing_test_body_is_not_overridden(pytester):
|
|
61
|
+
body = TEST_FILE.format(name="x", want=1) + " assert False, 'the body failed'\n"
|
|
62
|
+
result, logdir = run(pytester, body=body)
|
|
63
|
+
result.assert_outcomes(failed=1)
|
|
64
|
+
result.stdout.fnmatch_lines(["*the body failed*"])
|
|
65
|
+
result.stdout.no_fnmatch_line("*wireskein: 1 NG*")
|
|
66
|
+
assert (run_dir(logdir, "x") / "report.json").exists() # still verified and recorded
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_unchecked_does_not_fail(pytester):
|
|
70
|
+
body = TEST_FILE.format(name="x", want=0).replace('level("P0", 0)', 'level("NOT_CAPTURED", 0)')
|
|
71
|
+
result, _ = run(pytester, body=body)
|
|
72
|
+
result.assert_outcomes(passed=1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_unused_recorder_writes_no_report(pytester):
|
|
76
|
+
body = "def test_x(ws_run):\n pass\n"
|
|
77
|
+
result, logdir = run(pytester, body=body)
|
|
78
|
+
result.assert_outcomes(passed=1)
|
|
79
|
+
assert {p.name for p in run_dir(logdir, "x").iterdir()} == {"run.json"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_bad_mode_is_a_usage_error(pytester):
|
|
83
|
+
pytester.makeini("[pytest]\nwireskein_verify = maybe\n")
|
|
84
|
+
result, _ = run(pytester)
|
|
85
|
+
assert result.ret != 0
|
|
86
|
+
result.stderr.fnmatch_lines(["*wireskein_verify must be one of fail, report, off*"])
|