Some fixes and tidies for testing both online and in unit_tests

This commit is contained in:
nomdetom
2026-06-03 02:47:23 +01:00
parent c3a46d4d85
commit 5d55353939
11 changed files with 135 additions and 44 deletions
+57 -4
View File
@@ -15,6 +15,30 @@ pio test -e native -f test_your_module
pio test -e native -f test_your_module -vvv
```
**Never pipe through `| tail -N` to shorten output.** PlatformIO prints build errors at the top of output and test results at the bottom; `tail` will show stale cached results from a prior successful build while hiding the compile error that caused the current run to fail.
**Preferred pattern — redirect to file, then grep:**
```bash
# Redirect all output to a file; grep for errors and results after it exits
pio test -e native -f test_your_module > /tmp/test_out.txt 2>&1
echo "exit: $?"
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
tail -15 /tmp/test_out.txt
```
Why: piping through `| grep` line-buffers the output and suppresses all progress until the process exits, making it look hung. The redirect approach lets the build stream normally while still giving you filtered results afterwards.
**`externally-managed-environment` error on Ubuntu/Debian:**
If `pio test` fails immediately with `error: externally-managed-environment`, the system `pio` binary is using the OS Python which newer distros lock down. Use PlatformIO's own venv instead:
```bash
~/.platformio/penv/bin/python -m platformio test -e native -f test_your_module > /tmp/test_out.txt 2>&1
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
tail -15 /tmp/test_out.txt
```
### Helper Scripts (Useful Shortcuts)
These wrappers are handy when local host dependencies are missing or when you want repeatable commands.
@@ -185,20 +209,34 @@ Subclass the module under test to make protected methods callable and private me
class YourModuleTestShim : public YourModule
{
public:
// Expose protected methods
// Pull protected methods into public scope via using.
// IMPORTANT: using requires the method to be protected (or public) in the base —
// friend alone does NOT satisfy this. See pitfall #6.
using YourModule::runOnce;
using YourModule::someProtectedMethod;
// Access private members via friend (see below)
// Wrap private members with setter methods (friend grants direct access here).
void setPrivateField(int x) { privateField = x; }
};
```
In the module header, grant friend access under the `UNIT_TEST` define (set automatically by PlatformIO's test framework):
For methods you want to expose via `using`, use the conditional access-specifier pattern in the header — **not** plain `friend`:
```cpp
// In YourModule.h, inside the class body:
#ifdef UNIT_TEST
#ifdef PIO_UNIT_TESTING
protected:
#else
private:
#endif
bool someMethod();
```
For private _member variables_ that a shim setter needs to touch directly, `friend` is sufficient (no `using` involved):
```cpp
// In YourModule.h, inside the class body:
#ifdef PIO_UNIT_TESTING
friend class YourModuleTestShim;
#endif
```
@@ -284,6 +322,21 @@ Fixed-size data structures (hash sets, ring buffers) overflow when tests inject
**Fix:** Simulate multiple realistic time windows rather than one massive burst. Let adaptive mechanisms (if any) self-tune over several rolls.
### 6. Granting test access to private/protected members
PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several production headers (`TransmitHistory.h`, `CryptoEngine.h`, `MQTT.h`, `RTC.h`) use this to gate test-only visibility changes. PlatformIO also defines `UNIT_TEST` in the same builds for backward compatibility, but that spelling is deprecated — always use `PIO_UNIT_TESTING` in new code. The established pattern for exposing a private method to a test shim **without widening production visibility**:
```cpp
#ifdef PIO_UNIT_TESTING
protected:
#else
private:
#endif
bool myMethod();
```
**Critical C++ rule:** a `using` declaration in a derived class (e.g. `using Base::myMethod`) requires `myMethod` to be `protected` or `public` in the base — `friend` alone does **not** satisfy this. Adding `friend class TestShim` while leaving the method `private` will still fail to compile. Use the conditional access-specifier pattern above, not `friend`.
## setUp/tearDown Checklist
- [ ] Create and clear MockNodeDB (if needed)
+25 -1
View File
@@ -36,7 +36,6 @@ static MockMeshService *mockMeshService;
// -----------------------------------------------------------------------
// getRegion() tests
// -----------------------------------------------------------------------
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
static void test_getRegion_returnsCorrectRegion_US()
{
@@ -104,6 +103,29 @@ static void test_validateConfigRegion_unsetRegionReturnsTrue()
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
}
static void test_validateConfigRegion_unknownCodeReturnsFalse()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)255;
devicestate.owner.is_licensed = false;
// Unknown code is not in the regions table; getRegion() returns the UNSET sentinel,
// whose .code != 255, so validateConfigRegion should reject it.
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
}
static void test_validateConfigRegion_anotherUnknownCodeReturnsFalse()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)99;
devicestate.owner.is_licensed = true;
// Unknown code should be rejected even when owner is licensed.
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
}
// -----------------------------------------------------------------------
// Shadow tables for testing (preset lists → profiles → regions → lookup)
// -----------------------------------------------------------------------
@@ -937,6 +959,8 @@ void setup()
// validateConfigRegion()
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
RUN_TEST(test_validateConfigRegion_unknownCodeReturnsFalse);
RUN_TEST(test_validateConfigRegion_anotherUnknownCodeReturnsFalse);
// Shadow table tests
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
-6
View File
@@ -27,12 +27,6 @@
#include <utility>
#include <variant>
#if defined(UNIT_TEST)
#define IS_RUNNING_TESTS 1
#else
#define IS_RUNNING_TESTS 0
#endif
namespace
{
// Minimal router needed to receive messages from MQTT.
-6
View File
@@ -5,12 +5,6 @@
#ifdef ARCH_PORTDUINO
#include "configuration.h"
#if defined(UNIT_TEST)
#define IS_RUNNING_TESTS 1
#else
#define IS_RUNNING_TESTS 0
#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
#include "modules/SerialModule.h"