Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/apis/pytest-embedded.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
:undoc-members:
:show-inheritance:

.. automodule:: pytest_embedded.group
:members:
:undoc-members:
:show-inheritance:

.. automodule:: pytest_embedded.dut_factory
:members:
:undoc-members:
Expand Down
139 changes: 138 additions & 1 deletion docs/usages/expecting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Expecting Functions
#####################

In testing, most of the work involves expecting a certain string or pattern and then making assertions. This is supported by the functions :func:`~pytest_embedded.dut.Dut.expect`, :func:`~pytest_embedded.dut.Dut.expect_exact`, and :func:`~pytest_embedded.dut.Dut.expect_unity_test_output`.
In testing, most of the work involves expecting a certain string or pattern and then making assertions. This is supported by the functions :func:`~pytest_embedded.dut.Dut.expect`, :func:`~pytest_embedded.dut.Dut.expect_exact`, :class:`~pytest_embedded.group.DutGroup` (multi-DUT synchronization), and :func:`~pytest_embedded.dut.Dut.expect_unity_test_output`.

All of these functions accept the following keyword arguments:

Expand Down Expand Up @@ -186,6 +186,143 @@ As with the :func:`~pytest_embedded.dut.Dut.expect` function, the ``pattern`` ar
for _ in range(2):
dut.expect_exact(pattern_list)

***************************
Multi-DUT Synchronization
***************************

When you use ``--count N`` (or equivalent), each board has its own serial stream and its own :class:`~pytest_embedded.dut.Dut` instance. Waiting for readiness on each device with separate ``expect`` calls works, but:

- Sequential calls use **per-call** timeouts, so two ``expect_exact(..., timeout=120)`` lines can behave like a much larger wall-clock budget than a single 120s deadline.
- The **slowest** device should not delay matching on others more than your chosen global timeout.

:class:`~pytest_embedded.group.DutGroup`
========================================

``DutGroup`` is a transparent proxy: **every method** available on a single :class:`~pytest_embedded.dut.Dut` can be called on the group. The call runs on all members **in parallel** and returns a list of per-DUT results.

.. code:: python

from pytest_embedded import DutGroup

def test_two_boards(dut):
group = DutGroup(dut[0], dut[1])
# or from a list:
group = DutGroup(*dut)

It is also available as ``Dut.DutGroup`` for discoverability.

expect / expect_exact
---------------------

``expect`` and ``expect_exact`` support both **broadcast** (one pattern for all DUTs) and **per-DUT** patterns (N patterns for N DUTs), all running in parallel:

.. code:: python

# Broadcast -- same pattern to every DUT
group.expect_exact("[READY]", timeout=120)

# Per-DUT patterns -- one per DUT, in constructor order
group.expect_exact("[AP] ready", "[CLIENT] ready", timeout=120)

# Regex -- also supports broadcast and per-DUT forms
results = group.expect(r"IP=(\S+)", timeout=10)
ip0 = results[0].group(1).decode()
ip1 = results[1].group(1).decode()

# Same as :class:`~pytest_embedded.dut.Dut`: a single pattern may use the keyword form
group.expect_exact(pattern="[READY]", timeout=120)

Other methods
-------------

Any other :class:`~pytest_embedded.dut.Dut` method called on the group is forwarded with the **same arguments** to every DUT in parallel:

.. code:: python

group.write(ssid)

For per-DUT arguments on non-expect methods, index into the group:

.. code:: python

group[0].write(ap_config)
group[1].write(client_config)

Container protocol
------------------

``DutGroup`` supports indexing, iteration, and length:

.. code:: python

group[0] # first DUT
group[-1] # last DUT
len(group) # number of DUTs
list(group) # iterate over DUTs
group.duts # underlying tuple (read-only)

Non-callable attributes are returned as a list:

.. code:: python

procs = group.pexpect_proc # [proc_0, proc_1, ...]

Names and clearer errors
------------------------

Pass optional **member** labels and an optional **group** label so logs and failures are easy to read:

.. code:: python

group = DutGroup(*dut, names=("ap", "client"), group_name="wifi_ap")
# group.names -> ("ap", "client"); group.group_name -> "wifi_ap"

If you omit ``names``, members default to ``dut-0``, ``dut-1``, … (aligned with per-DUT log file names when using ``--count``).

When any parallel call fails on one DUT, pytest-embedded raises :exc:`pytest_embedded.group.DutGroupMemberError`. Its message and attributes identify the member (``member_name``, ``member_index``) and group (``group_name``), and the original error (for example :exc:`pexpect.TIMEOUT`) is chained as :attr:`__cause__`. A structured line is also written to the Python logger at ERROR (including the underlying exception context).

Full example
------------

.. code:: python

from pytest_embedded import DutGroup

def test_wifi_ap(dut):
group = DutGroup(*dut)

# Phase 1: wait for both devices to be ready
group.expect_exact("[READY]", timeout=120)

# Phase 2: exchange SSID
group.expect_exact("Send SSID:", timeout=10)
group.write(ap_ssid)

# Phase 3: exchange password
group.expect_exact("Send Password:", timeout=10)
group.write(ap_password)

# Phase 4: verify connection
results = group.expect(r"IP=(\S+)", timeout=30)
for r in results:
assert r.group(1) != b""

Phase synchronization
=====================

``DutGroup`` methods can be called **multiple times** in one test to synchronize phases. Each call blocks until every DUT has matched before continuing. After a successful match, those substrings are consumed from each DUT's buffer; emit new output for the next phase.

.. code:: python

group = DutGroup(*dut)
group.expect_exact("Init OK", timeout=30)
group.expect_exact("Server started", timeout=10)
group.expect_exact("Connected", timeout=60)

.. note::

If one DUT fails, pending work is cancelled where possible; expects that have already started may still run until they match or time out, because pexpect cannot always be interrupted from another thread. The failure is reported as :exc:`~pytest_embedded.group.DutGroupMemberError` with the underlying error as :attr:`~BaseException.__cause__`.

***********************************************************
:func:`~pytest_embedded.dut.Dut.expect_unity_test_output`
***********************************************************
Expand Down
3 changes: 2 additions & 1 deletion pytest-embedded/pytest_embedded/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from .app import App
from .dut import Dut
from .dut_factory import DutFactory
from .group import DutGroup, DutGroupMemberError

__all__ = ['App', 'Dut', 'DutFactory']
__all__ = ['App', 'Dut', 'DutFactory', 'DutGroup', 'DutGroupMemberError']

__version__ = '2.7.0'
5 changes: 5 additions & 0 deletions pytest-embedded/pytest_embedded/dut.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pexpect

from .app import App
from .group import DutGroup
from .log import MessageQueue, PexpectProcess
from .unity import UNITY_SUMMARY_LINE_REGEX, TestSuite
from .utils import Meta, _InjectMixinCls, remove_asci_color_code, to_bytes, to_list
Expand Down Expand Up @@ -232,3 +233,7 @@ def run_all_single_board_cases(
requires enable service ``idf``
"""
pass


#: Alias for :class:`~pytest_embedded.group.DutGroup` for discoverability.
Dut.DutGroup = DutGroup
Loading
Loading