Skip to content
Merged
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
16 changes: 16 additions & 0 deletions tests/runner/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ def run_action(action, driver_instance):
response = input(f"\n [MANUAL] {prompt} [y/n] ")
return response.strip().lower() == "y"

if action_type == "interactive":
# Prompt first, then call method (used for hold-button-and-read tests)
import sys
if not sys.stdin.isatty():
return None # skip in non-interactive mode
prompt = action.get("pre_prompt", "Perform action then press Enter")
input(f"\n [INTERACTIVE] {prompt} ")
method_name = action["method"]
Comment thread
nedseb marked this conversation as resolved.
args = action.get("args", [])
method = getattr(driver_instance, method_name)
return method(*args)

if action_type == "hardware_script":
# hardware_script is hardware-only; skip in mock mode
return None

raise ValueError(f"Unknown action type: {action_type}")


Expand Down
37 changes: 37 additions & 0 deletions tests/runner/mpremote_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,43 @@ def read_register(self, i2c_config, i2c_address, register, nbytes=1):
return result[0]
return result

def run_script(
self, driver_name, driver_class, i2c_config, script,
module_name=None, hardware_init=None, i2c_address=None,
):
"""Run a custom MicroPython script with driver context.

The script has access to ``i2c`` and ``dev`` variables and must
set a ``result`` variable. The method returns the JSON-decoded
value of ``result``.
Comment thread
nedseb marked this conversation as resolved.

The script must not print anything: any additional output on
stdout will cause JSON parsing to fail.

When ``hardware_init`` is provided it takes precedence over
``i2c_address`` for device construction.
"""
mod = module_name or driver_name
i2c_init = _i2c_init_code(i2c_config)
if hardware_init is not None:
dev_init = hardware_init + "\n"
elif i2c_address is not None:
dev_init = f"dev = {driver_class}(i2c, address={i2c_address!r})\n"
else:
dev_init = f"dev = {driver_class}(i2c)\n"
code = (
f"import json\n"
f"{i2c_init}\n"
f"from {mod}.device import {driver_class}\n"
f"{dev_init}"
f"{script}\n"
f"print(json.dumps(result))"
)
output = self._run(code, mount_dir=self._driver_dir(driver_name))
# Parse only the last non-empty line as JSON to ignore stray output
last_line = output.strip().rsplit("\n", 1)[-1]
return json.loads(last_line)

Comment thread
nedseb marked this conversation as resolved.
def scan_bus(self, i2c_config):
"""Scan I2C bus and return list of addresses."""
i2c_init = _i2c_init_code(i2c_config)
Expand Down
268 changes: 268 additions & 0 deletions tests/scenarios/mcp23009e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
driver: mcp23009e
driver_class: MCP23009E
i2c_address: 0x20

# I2C config for hardware tests (STeaMi board - STM32WB55)
i2c:
id: 1

# Extra constructor pins (FakePin instances for mock tests)
mock_pins:
reset_pin: 0

# Custom hardware init (MCP23009E requires a reset pin)
hardware_init: |
from machine import Pin
dev = MCP23009E(i2c, address=0x20, reset_pin=Pin("RST_EXPANDER", Pin.OUT))

# Register values for mock tests
mock_registers:
# IODIR (0x00): all inputs by default (0xFF)
0x00: 0xFF
# IPOL (0x01): no inversion
0x01: 0x00
# GPINTEN (0x02): interrupts disabled
0x02: 0x00
# DEFVAL (0x03): default
0x03: 0x00
# INTCON (0x04): default
0x04: 0x00
# IOCON (0x05): default config
0x05: 0x00
# GPPU (0x06): no pull-ups
0x06: 0x00
# INTF (0x07): no interrupt flags
0x07: 0x00
# INTCAP (0x08): default
0x08: 0x00
# GPIO (0x09): D-PAD buttons (bits 4-7 as inputs with pull-ups)
0x09: 0xF0
# OLAT (0x0A): default
0x0A: 0x00

tests:
- name: "Read IODIR default value"
action: call
method: get_iodir
expect: 0xFF
mode: [mock, hardware]

- name: "Read GPIO register"
action: call
method: get_gpio
expect_not_none: true
mode: [mock, hardware]

- name: "Read individual GPIO level"
action: call
method: get_level
args: [0]
expect_not_none: true
mode: [mock]

- name: "D-PAD buttons state"
action: manual
display:
- method: get_level
args: [7]
label: "UP (GP7)"
unit: "(1=relâché, 0=appuyé)"
- method: get_level
args: [5]
label: "DOWN (GP5)"
unit: "(1=relâché, 0=appuyé)"
- method: get_level
args: [6]
label: "LEFT (GP6)"
unit: "(1=relâché, 0=appuyé)"
- method: get_level
args: [4]
label: "RIGHT (GP4)"
unit: "(1=relâché, 0=appuyé)"
prompt: "Tous les boutons sont-ils relâchés (valeur 1) ?"
expect_true: true
mode: [hardware]

# --- Polling tests (one per D-PAD button) ---
# LED_GREEN signals when the board is ready to read the button.

- name: "D-PAD UP button (polling)"
action: hardware_script
pre_prompt: "Test polling D-PAD : appuyez sur chaque bouton quand la LED verte s'allume (5s). Commençons par UP."
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_level(7) == 0:
detected = True
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD DOWN button (polling)"
action: hardware_script
pre_prompt: "Bouton DOWN"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_level(5) == 0:
detected = True
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD LEFT button (polling)"
action: hardware_script
pre_prompt: "Bouton LEFT"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_level(6) == 0:
detected = True
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD RIGHT button (polling)"
action: hardware_script
pre_prompt: "Bouton RIGHT"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_level(4) == 0:
detected = True
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

# --- Interrupt tests (one per D-PAD button) ---
# LED_GREEN signals when the board is ready; polls INTF register.

- name: "D-PAD UP button (interrupt)"
action: hardware_script
pre_prompt: "Test interrupt D-PAD : appuyez brièvement sur chaque bouton quand la LED verte s'allume (5s). Commençons par UP."
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
dev.interrupt_on_change(7, lambda l: None)
dev.get_gpio()
led.on()
Comment thread
nedseb marked this conversation as resolved.
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_intf() & (1 << 7):
detected = True
dev.get_intcap()
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD DOWN button (interrupt)"
action: hardware_script
pre_prompt: "Bouton DOWN"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
dev.interrupt_on_change(5, lambda l: None)
dev.get_gpio()
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_intf() & (1 << 5):
detected = True
dev.get_intcap()
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD LEFT button (interrupt)"
action: hardware_script
pre_prompt: "Bouton LEFT"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
dev.interrupt_on_change(6, lambda l: None)
dev.get_gpio()
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_intf() & (1 << 6):
detected = True
dev.get_intcap()
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]

- name: "D-PAD RIGHT button (interrupt)"
action: hardware_script
pre_prompt: "Bouton RIGHT"
wait: false
script: |
from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
led = Pin("LED_GREEN", Pin.OUT)
dev.interrupt_on_change(4, lambda l: None)
dev.get_gpio()
led.on()
start = ticks_ms()
detected = False
while ticks_diff(ticks_ms(), start) < 5000:
if dev.get_intf() & (1 << 4):
detected = True
dev.get_intcap()
break
sleep_ms(50)
led.off()
result = detected
expect_true: true
mode: [hardware]
38 changes: 38 additions & 0 deletions tests/test_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def test_scenario(scenario, test, mode, port):
display.get("args"),
module_name=scenario.get("module_name"),
hardware_init=scenario.get("hardware_init"),
i2c_address=scenario.get("i2c_address"),
)
label = display.get("label", display["method"])
unit = display.get("unit", "")
Expand All @@ -118,13 +119,50 @@ def test_scenario(scenario, test, mode, port):
test.get("args"),
module_name=scenario.get("module_name"),
hardware_init=scenario.get("hardware_init"),
i2c_address=scenario.get("i2c_address"),
)
elif action == "read_register":
result = bridge.read_register(
scenario["i2c"],
scenario["i2c_address"],
test["register"],
)
elif action == "interactive":
# Prompt user first, then call method
import sys
if not sys.stdin.isatty():
pytest.skip("interactive test requires interactive mode (-s)")
pre_prompt = test.get("pre_prompt", "Perform action then press Enter")
input(f" [INTERACTIVE] {pre_prompt} ")
Comment thread
nedseb marked this conversation as resolved.
result = bridge.call_method(
scenario["driver"],
scenario["driver_class"],
scenario["i2c"],
test["method"],
test.get("args"),
module_name=scenario.get("module_name"),
hardware_init=scenario.get("hardware_init"),
i2c_address=scenario.get("i2c_address"),
)
elif action == "hardware_script":
import sys
if not sys.stdin.isatty():
pytest.skip("hardware_script test requires interactive mode (-s)")
pre_prompt = test.get("pre_prompt")
if pre_prompt:
if test.get("wait", True):
input(f" [INTERACTIVE] {pre_prompt} ")
else:
print(f" [INTERACTIVE] {pre_prompt}")
result = bridge.run_script(
scenario["driver"],
scenario["driver_class"],
scenario["i2c"],
test["script"],
module_name=scenario.get("module_name"),
hardware_init=scenario.get("hardware_init"),
i2c_address=scenario.get("i2c_address"),
)
else:
pytest.fail(f"Unknown action: {action}")

Expand Down
Loading