Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add Renode device support.
- Add opt-in resource leak detection fixture.

## 0.0.3

Expand Down
74 changes: 74 additions & 0 deletions Documentation/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,80 @@ Remaining tests are skipped if all retries fail.
max_retries: 3 # reboot attempts before skipping remaining tests
base_delay: 2.0 # seconds between retries (doubles each attempt)

.. _resource-monitoring:

Resource Monitoring
===================

Tests that request the ``resources`` fixture get their resource usage
sampled before and after the test body. A delta that does not settle fails
the test at teardown. The feature is disabled unless enabled here and needs
``CONFIG_FS_PROCFS`` on the target.

.. code-block:: yaml

config:
resources:
enabled: True
probes: [tasks, fds] # default
settle_retries: 3 # re-samples while a delta remains
settle_delay: 1.0 # seconds between re-samples
heap_tolerance: 0 # bytes of heap growth tolerated
cpuload_max: 10 # percent, cpuload probe only

Each probe runs one NSH command and compares the two samples:

.. list-table::
:header-rows: 1

* - Probe
- Command
- Reports
- Requires
* - ``heap``
- ``free``
- ``used`` grew past ``heap_tolerance`` or ``nused`` grew, per heap
- ``CONFIG_NSH_DISABLE_FREE`` unset
* - ``tasks``
- ``ps``
- a task or thread present after the test but not before
- ``CONFIG_NSH_DISABLE_PS`` unset
* - ``fds``
- ``cat /proc/self/group/fd``
- a descriptor opened in the shell task group and not closed
- ``CONFIG_NSH_DISABLE_CAT`` unset
* - ``iob``
- ``cat /proc/iobinfo``
- fewer free network buffers
- ``CONFIG_MM_IOB``
* - ``mempool``
- ``cat /proc/mempool``
- ``nused`` grew, per pool
- ``CONFIG_MM_HEAP_MEMPOOL``
* - ``net``
- ``cat /proc/net/tcp``, ``cat /proc/net/udp``
- a new connection
- ``CONFIG_NET``
* - ``cpuload``
- ``cat /proc/cpuload``
- load above ``cpuload_max`` after the test
- ``CONFIG_SCHED_CPULOAD``

A probe whose Kconfig requirements (including the matching
``CONFIG_FS_PROCFS_EXCLUDE_*`` option) are not met on every product is
dropped with a warning. The final delta is written to the device log and to
the ``resources`` user property of the test report. Tests that crash or
busy-loop are not checked.

``tasks`` keys entries by TID and command name; set ``CONFIG_TASK_NAME_SIZE``
above zero so a leaked task is reported by name rather than ``<noname>``.

``heap`` is not a default: first-use allocations (stdio buffers, work
queues) and shell state (environment variables, ``OLDPWD``) show up as
growth, so it reports more than true leaks. Enable it per test with the
``resources`` marker and a ``heap_tolerance`` that fits, or repeat the run
with ``--loops`` and look for growth that recurs on every loop.

Signal Handlers
===============

Expand Down
24 changes: 23 additions & 1 deletion Documentation/writing-test-cases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,26 @@ NTFC provides built-in fixtures and pytest marks for test development.

Built-in Fixtures:

TODO
.. list-table::
:header-rows: 1

* - Fixture
- Purpose
* - ``core``
- Current core name, parametrized with ``--run_in_cores``
* - ``switch_to_core``
- Switch to the parametrized core in SMP mode and back afterwards
* - ``resources``
- Fail the test if resources are not released (see
:ref:`resource-monitoring`). List it after ``switch_to_core`` so
sampling happens on the switched core.

.. code-block:: python

@pytest.mark.resources(probes=["heap", "tasks"], heap_tolerance=64)
def test_no_leak(resources):
ret = pytest.product.sendCommand("app", ["done"], timeout=15)
assert ret == 0

Custom Fixtures:

Expand Down Expand Up @@ -300,6 +319,9 @@ Available Marks:
* - ``@pytest.mark.skip()``
- Skip test conditionally
- ``@pytest.mark.skip(reason="not ready")``
* - ``@pytest.mark.resources()``
- Override resource monitor options for one test
- ``@pytest.mark.resources(probes=["heap"])``
* - ``@pytest.mark.parser_binary()``
- Discover and run test framework tests (cmocka / custom) as pytest items
- ``@pytest.mark.parser_binary("bin", list_pattern=..., result_pattern=...)``
Expand Down
22 changes: 22 additions & 0 deletions src/ntfc/envconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ def heartbeat(self) -> Dict[str, Any]:
# Merge with defaults
return {**default_config, **heartbeat_cfg}

@property
def resources(self) -> Dict[str, Any]:
"""Return resource monitoring configuration.

:return: Dictionary with keys: 'enabled' (bool), 'probes' (list),
'settle_retries' (int), 'settle_delay' (float),
'heap_tolerance' (int), 'cpuload_max' (float). Defaults to
disabled if not configured.
"""
default_config = {
"enabled": False,
"probes": ["tasks", "fds"],
"settle_retries": 3,
"settle_delay": 1.0,
"heap_tolerance": 0,
"cpuload_max": 10,
}
config = self._cfg_values.get("config", {})
resources_cfg = config.get("resources", {})

return {**default_config, **resources_cfg}

@property
def recovery(self) -> Dict[str, Any]:
"""Return device recovery configuration.
Expand Down
19 changes: 4 additions & 15 deletions src/ntfc/log/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

"""Report generation module for NTFC."""

import copy
import glob
import html
import os
Expand Down Expand Up @@ -118,22 +119,10 @@ def _split_xml_by_module( # noqa: C901
new_testsuite.set("time", str(time_total))
new_testsuite.set("name", module_name)

# Add all test cases
# Add all test cases, including nested elements such as
# <properties><property/></properties>
for testcase in testcases:
# Deep copy the testcase element
new_testcase = ElementTree.SubElement(
new_testsuite, "testcase"
)
for attr, value in testcase.attrib.items():
new_testcase.set(attr, value)

# Copy all child elements
for child in testcase:
new_child = ElementTree.SubElement(new_testcase, child.tag)
for attr, value in child.attrib.items():
new_child.set(attr, value)
if child.text:
new_child.text = child.text
new_testsuite.append(copy.deepcopy(testcase))

# Write per-module XML file
output_file = os.path.join(
Expand Down
2 changes: 2 additions & 0 deletions src/ntfc/pytest/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ def pytest_configure(self, config: pytest.Config) -> None:
"extra_opts: Additional parameters for testing "
"(e.g., --run_in_cores=cpu1,cpu2)",
"config_value_check: Validate complex configuration strings",
"resources: Override resource monitor options for one test "
"(e.g., probes=['heap'], heap_tolerance=64)",
]

for m in markers:
Expand Down
8 changes: 7 additions & 1 deletion src/ntfc/pytest/mypytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .configure import PytestConfigPlugin
from .gtest_plugin import GtestParserPlugin
from .parsers import ParserPlugin
from .resources_plugin import ResourcePlugin
from .runner import RunnerPlugin
from .signal_plugin import SignalPlugin

Expand Down Expand Up @@ -390,6 +391,9 @@ def runner(
# run pytest with our custom test plugin
runner = RunnerPlugin(nologs)

# resource leak detection fixture
resources = ResourcePlugin(self._config, pytest.products)

# optional debug plugins (coredump collection)
debug = get_debug_setup(pytest.products)
self._ptconfig.set_restart_gdb(debug.restart_all_controllers)
Expand All @@ -400,7 +404,9 @@ def runner(
try:
# attach GDB controllers after the devices are up
debug.start(pytest.result_dir)
return self._run(opt, [runner, collector, *debug.plugins])
return self._run(
opt, [runner, collector, resources, *debug.plugins]
)
finally:
debug.stop()
self._device_stop()
Expand Down
Loading
Loading