Files
timely-reference/test/src/tr_test/test.py
2020-07-05 17:27:22 -07:00

398 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (C) 2020 Max Regan
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import serial
import serial.tools.list_ports
import logging
import pylink
import time
import os
import sys
import re
import subprocess
PYTEST_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_FW_DIR = os.path.abspath(PYTEST_DIR + "../../../../firmware/")
SAMPLE_TIME_MS = 2500
SAMPLES_PER_SEC = 12000000
DRIVER = "fx2lafw"
TEST_START_TEXT = b"TEST_BEGIN\r\n"
TEST_PASS_TEXT = b"TEST_PASS\r\n"
TEST_FAIL_TEXT = b"TEST_FAIL\r\n"
@pytest.fixture
def logger():
logging.basicConfig()
return logging.getLogger(__name__)
@pytest.fixture
def context_factory():
def create_context(
fw_rel_path: str,
mcu: str = "STM32L010C6",
addr: int = 0x8000000,
leave_halted: bool = False,
):
ports = [
p
for p in serial.tools.list_ports.comports()
if p.product == "FT232R USB UART"
]
if len(ports) == 0:
raise RuntimeError("No serial devices found")
if len(ports) > 1:
raise RuntimeError(
"Too many serial devices, not sure which to use. "
"This should be made configurable"
)
jlink = pylink.jlink.JLink()
jlink.open()
jlink.disable_dialog_boxes()
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
jlink.connect(mcu)
fw = DEFAULT_FW_DIR + "/" + fw_rel_path
logging.info("Flashing {}...".format(fw))
jlink.flash_file(fw, addr)
logging.info("Flashing done.")
assert jlink.halted()
jlink.reset(halt=True)
serial_dev = serial.Serial(port=ports[0].device, baudrate=115200, timeout=2)
if leave_halted:
return serial_dev, jlink
# Start test test, and check that it started correctly over serial
jlink.reset(halt=False)
while True:
try:
logging.info("Waiting for firmware to start...")
assert serial_dev.read_until(TEST_START_TEXT).endswith(
TEST_START_TEXT
), "Timed out starting test firmware application"
logging.debug("Test execution started")
except serial.serialutil.SerialException:
continue
break
return serial_dev, jlink
return create_context
def measure_frequency(
period: float,
pin_name: str,
executable: str = "sigrok-cli",
driver_name: str = "fx2lafw",
trigger: str = "r",
):
cmd = [
executable,
"-C",
pin_name,
"-d",
driver_name,
"-c",
"samplerate=1M",
"--time",
"{}ms".format(int(period * 1000)),
"-t",
"{}={}".format(pin_name, trigger),
"-P",
"timing:data={}".format(pin_name),
"-A",
"timing=time",
]
print("sigrok-cli cmd {}".format(cmd))
proc = subprocess.run(cmd, capture_output=True, check=True)
lines = proc.stdout.decode("utf-8").split("\n")
reg = re.compile(".*:\\W(\\d+.\\d+)\\W(\\w+)")
periods = []
for line in lines:
m = reg.match(line)
if not m:
break
num = float(m.groups(1)[0])
units = m.groups(1)[1]
if units == "s":
periods.append(num)
elif units == "ms":
periods.append(num / 1000)
elif units == "μs":
periods.append(num / 1000000)
else:
assert False, "Couldnt find units in line '{}', units were '{}'".format(
line, units
)
return periods[::2], periods[1:][::2]
def test_meta_pass(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/pass.bin")
text = serial_dev.read_until(TEST_PASS_TEXT)
print("Got serial output: {}".format(text))
assert text.endswith(TEST_PASS_TEXT)
def test_meta_fail(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/fail.bin")
text = serial_dev.read_until(TEST_PASS_TEXT)
print("Got serial output: {}".format(text))
assert not text.endswith(TEST_PASS_TEXT)
def test_meta_timeout(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/timeout.bin")
text = serial_dev.read_until(TEST_PASS_TEXT)
assert not text.endswith(TEST_PASS_TEXT)
def test_meta_nostart(context_factory, logger):
with pytest.raises(AssertionError):
serial_dev, jlink = context_factory("Test/Apps/no_start.bin")
def test_watch(context_factory, logger):
serial_dev, jlink = context_factory("Application/main.bin", leave_halted=True)
jlink.reset(halt=False)
def test_set_time(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/set_time.bin")
text = serial_dev.read_until(TEST_PASS_TEXT)
print("Text:", text.decode())
assert text.endswith(TEST_PASS_TEXT)
def test_periodic_alarms(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/periodic_alarms.bin")
serial_dev.timeout = 6
text = serial_dev.read_until(TEST_PASS_TEXT)
print("Text:", text.decode())
assert text.endswith(TEST_PASS_TEXT)
def test_button_slow(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/button.bin")
serial_dev.timeout = 0.3
ASSERTED = True
serial_dev.dtr = not ASSERTED
while (line := serial_dev.readline()) is not None and len(line) > 0:
pass
for _ in range(5):
serial_dev.dtr = ASSERTED
press_line = serial_dev.readline()
serial_dev.dtr = not ASSERTED
release_line = serial_dev.readline()
assert press_line == b"up:pressed\r\n"
assert release_line == b"up:released\r\n"
def test_button_fast(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/button.bin")
serial_dev.timeout = 0.3
ASSERTED = True
serial_dev.dtr = not ASSERTED
time.sleep(0.3)
serial_dev.timeout = 0
while (line := serial_dev.readline()) is not None and len(line) > 0:
pass
for _ in range(25):
serial_dev.dtr = ASSERTED
time.sleep(0.075)
serial_dev.dtr = not ASSERTED
time.sleep(0.075)
serial_dev.timeout = 0.3
for _ in range(25):
press_line = serial_dev.readline()
release_line = serial_dev.readline()
assert press_line == b"up:pressed\r\n"
assert release_line == b"up:released\r\n"
serial_dev.timeout = 0
assert serial_dev.readline() == b""
def test_button_lowpower(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/button_lowpower.bin")
serial_dev.timeout = 0.3
ASSERTED = True
serial_dev.dtr = not ASSERTED
while (line := serial_dev.readline()) is not None and len(line) > 0:
pass
for _ in range(5):
serial_dev.dtr = ASSERTED
press_line = serial_dev.readline()
serial_dev.dtr = not ASSERTED
release_line = serial_dev.readline()
assert press_line == b"up:pressed\r\n"
assert release_line == b"up:released\r\n"
def test_clock(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/clock.bin")
EXPECTED_RUNTIME = 10
TOLERANCE = 0.2
serial_dev.timeout = EXPECTED_RUNTIME * 1.2
START_MARKER = b"GO\r\n"
END_MARKER = b"STOP\r\n"
start_text = serial_dev.read_until(START_MARKER)
start = time.monotonic()
print("Start text:", start_text)
assert start_text.endswith(START_MARKER)
end_text = serial_dev.read_until(END_MARKER)
end = time.monotonic()
print("Time:", end - start)
assert end_text.endswith(END_MARKER)
delta = end - start
logger.info("Serial text: {}".format(start_text))
logger.info("Serial text: {}".format(end_text))
logger.info("Start time: {}".format(start))
logger.info("End time: {}".format(end))
logger.info("Delta time: {}".format(delta))
# TODO: Using a single pin, instead of UART, would make this more
# accurate. Add support via sigrok.
assert abs(delta - EXPECTED_RUNTIME) < TOLERANCE
def test_wakeup_irq(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/wakeup_irq.bin")
serial_dev.timeout = 65
pattern = re.compile("Requested=(\\d*) Actual=(\\d*) Wakeups=(\\d*)")
while True:
line = serial_dev.readline()
if line == TEST_PASS_TEXT:
break
line = line.decode("ascii", errors="ignore")
print(line.strip())
match = pattern.match(line)
assert match
req = int(match.group(1))
actual = int(match.group(2))
wakeups = int(match.group(3))
delta = req - actual
assert wakeups == 1, "Expected one wakeup per line of test output"
if req < 32000:
assert abs(delta < req * (2.0 / 100.0)) or (delta <= 1)
else:
# Delays > 32sec have reduced resolution (1 sec)
assert abs(delta) < 1000
def test_lptim(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/lptim.bin")
state0_periods, state1_periods = measure_frequency(1, "D0")
num_periods = min(len(state0_periods), len(state1_periods))
periods = [state0_periods[i] + state1_periods[i] for i in range(num_periods)]
freqs = list(map(lambda x: 1 / x, periods))
assert (
periods
), "No LPTIM state changes detected, is the right analyzer being used? Is the device connected?"
min_f = min(freqs)
max_f = max(freqs)
avg_f = sum(freqs) / len(freqs)
print("min_f:{}, max_f:{}, avg_f:{}".format(min_f, max_f, avg_f))
assert abs(avg_f - 50) < 0.25
assert min_f > 49
assert max_f < 51
def test_app_lowpower(context_factory, logger):
serial_dev, jlink = context_factory("Application/main.bin", leave_halted=True)
jlink.reset(halt=False)
state0_periods, state1_periods = measure_frequency(10, "D0")
num_periods = min(len(state0_periods), len(state1_periods))
periods = [state0_periods[i] + state1_periods[i] for i in range(num_periods)]
freqs = list(map(lambda x: 1 / x, periods))
assert (
periods
), "No debug pin state changes detected, is the right analyzer being used? Is the device connected?"
min_f = min(freqs)
max_f = max(freqs)
avg_f = sum(freqs) / len(freqs)
pct_sleep = sum(state1_periods) * 100 / sum(state0_periods + state1_periods)
print(
"min_f:{}, max_f:{}, avg_f:{}, pct_sleep:{}".format(
min_f, max_f, avg_f, pct_sleep
)
)
assert len(periods) >= 5
assert pct_sleep > 99.95, "Spent too much time awake"
def test_stop(context_factory, logger):
serial_dev, jlink = context_factory("Test/Apps/stop.bin")
serial_dev.timeout = 70
pattern = re.compile("Requested=(\\d*) Actual=(\\d*)")
while True:
line = serial_dev.readline()
if line == TEST_PASS_TEXT:
break
line = line.decode("ascii", errors="ignore")
print(line.strip())
match = pattern.match(line)
assert match
req = int(match.group(1))
actual = int(match.group(2))
delta = req - actual
if req < 32000:
assert abs(delta < req * (2.0 / 100.0)) or (delta <= 1)
else:
# Delays > 32sec have reduced resolution (1 sec)
assert abs(delta) < 1000
def main():
pytest.main(sys.argv)
if __name__ == "__main__":
main()