Files
firmware/test/TestUtil.cpp
T
Thomas Göttgens 34680833b8 fix(test): make the native-windows test suite build and run (#11482)
* fix(test): make the native-windows test suite build and run

pio test -e native-windows failed every suite at the build stage. Five
independent causes, all Windows-only:

- TestUtil.cpp called lstat(), which MinGW-w64 does not provide. The
  state-checkpoint walk added in #11322 is fenced with ARCH_PORTDUINO,
  which native-windows also satisfies, so all 53 suites failed to
  compile. Route it through a stat() shim on _WIN32.

- test_default, test_http_content_handler, test_meshpacket_serializer
  and test_serial define no setUp/tearDown and relied on the weak
  defaults PlatformIO emits in unity_config.c. GCC lowers a weak
  definition on PE-COFF to a weak external, leaving the symbol
  undefined, so it does not satisfy unity.c's reference and the link
  fails. Define them explicitly, as the other 49 suites already do.

- test_mqtt included <arpa/inet.h>, absent on MinGW, for htonl(). Use
  winsock2.h there.

- test_gps_update_scheduling uses TEST_ASSERT_DOUBLE_WITHIN. Unity
  omits double support unless UNITY_INCLUDE_DOUBLE is defined, so the
  assertion compiled to an unconditional failure. Define it for the
  env.

- test_getfiles_rejects_overlong_path is excluded on _WIN32. Overrunning
  the 228-byte file_name needs at least 229 bytes below the portduino
  root, and that root is already ~34 bytes, so every qualifying path
  passes the 260-byte MAX_PATH: the nested mkdir() fails, the file is
  never created, and getFiles() has nothing to drop. No component
  layout satisfies both limits.

Each of the seven suites that failed on Windows was verified
individually after the change. test_fscommon_getfiles still fails in a
full run, for a cause outside this change: rmDir() does not remove
directories on Windows, so empty dirs left by an earlier run survive
setUp() and make getFiles() report a depth truncation. That is a
pre-existing FSCommon bug, reported separately.

No Linux or macOS behaviour changes: every guard is _WIN32-only except
UNITY_INCLUDE_DOUBLE, which is scoped to env:native-windows.

* fix(test): define UNITY_INCLUDE_DOUBLE for every native env

The flag was scoped to env:native-windows, but the gap is not
Windows-specific. Verified on Debian with gcc against the Linux env's
own Unity 2.6.1 and PlatformIO's generated native unity_config:

  UNITY_INCLUDE_DOUBLE : NOT defined
  UNITY_EXCLUDE_DOUBLE : defined
  test_double_within:FAIL: Unity Double Precision Disabled

UNITY_INCLUDE_DOUBLE appears nowhere in the repo, the ini files, the
workflow, or PlatformIO's unity runner, which adds only
UNITY_INCLUDE_CONFIG_H. So TEST_ASSERT_DOUBLE_* is an always-failing
stub on Linux and macOS too, not only on Windows.

Moved to portduino_base.build_flags_common, which every native env
resolves: native, native-tft, native-fb, native-tft-debug, coverage,
coverage-event-policy, native-macos, native-windows and native-wasm.

This does change Linux and macOS: TEST_ASSERT_DOUBLE_* becomes a real
comparison instead of a stub. test_gps_update_scheduling is the only
suite using those macros and its arithmetic is integer-based and
bit-identical across platforms, so it should pass wherever it runs.
Note it currently reports PASSED on CI in 0.03s while emitting no Unity
output at all, so those assertions appear never to execute there; that
is tracked separately and is not addressed here.
2026-08-14 00:51:40 +00:00

199 lines
5.7 KiB
C++

// First, in its own block so the include sorter keeps it there: configuration.h supplies the
// variant defines mesh-pb-constants.h needs (portduino resolves MAX_NUM_NODES at runtime).
#include "configuration.h"
#include "SerialConsole.h"
#include "concurrency/OSThread.h"
#include "gps/RTC.h"
#include "TestUtil.h"
#if defined(ARDUINO)
#include <Arduino.h>
#else
#include <chrono>
#include <thread>
#endif
// The state checkpoint needs a POSIX directory walk, and only the host builds run these suites.
// Note ARDUINO *is* defined on portduino, so it is not the right guard here.
#if ARCH_PORTDUINO
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <map>
#include <string>
#include <sys/stat.h>
#endif
void initializeTestEnvironment()
{
concurrency::hasBeenSetup = true;
consoleInit();
#if ARCH_PORTDUINO
struct timeval tv;
tv.tv_sec = time(NULL);
tv.tv_usec = 0;
perhapsSetRTC(RTCQualityNTP, &tv);
#endif
concurrency::OSThread::setup();
// Baseline the sandbox before the first RUN_TEST, so writes made during suite setup are not
// charged to whichever test happens to run first.
testStateCheckpoint(nullptr, nullptr);
}
void testDelay(unsigned long ms)
{
#if defined(ARDUINO)
::delay(ms);
#else
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
#if !ARCH_PORTDUINO
void testStateCheckpoint(const char *, const char *) {}
#else
namespace
{
/// MinGW-w64 has no lstat(): Windows has no POSIX symlink stat, and nothing in a test sandbox
/// creates a symlink, so stat() sees the same thing for every entry walk() can reach.
#ifdef _WIN32
inline int lstatCompat(const char *path, struct stat *st)
{
return stat(path, st);
}
#else
inline int lstatCompat(const char *path, struct stat *st)
{
return lstat(path, st);
}
#endif
/// Content fingerprint, used only to answer "did this file change?". FNV-1a rather than a real
/// digest because the answer is a boolean and the files are a few KB of protobuf; nothing here
/// records a hash as an expected value, which is what would make this a snapshot test.
uint64_t fileFingerprint(const std::string &path)
{
FILE *f = fopen(path.c_str(), "rb");
if (!f)
return 0;
uint64_t h = 1469598103934665603ULL;
unsigned char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
for (size_t i = 0; i < n; i++) {
h ^= buf[i];
h *= 1099511628211ULL;
}
}
fclose(f);
return h;
}
void walk(const std::string &root, const std::string &rel, std::map<std::string, uint64_t> &out)
{
const std::string dirPath = rel.empty() ? root : root + "/" + rel;
DIR *d = opendir(dirPath.c_str());
if (!d)
return;
while (struct dirent *e = readdir(d)) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
const std::string childRel = rel.empty() ? std::string(e->d_name) : rel + "/" + e->d_name;
const std::string childPath = root + "/" + childRel;
struct stat st;
if (lstatCompat(childPath.c_str(), &st) != 0)
continue;
if (S_ISDIR(st.st_mode))
walk(root, childRel, out);
else if (S_ISREG(st.st_mode))
out[childRel] = fileFingerprint(childPath);
}
closedir(d);
}
/// "test/test_admin_radio/test_main.cpp" -> "test_admin_radio". The suite name is not otherwise
/// available to a test program - PlatformIO passes it to the *build*, not to the run.
std::string suiteFromPath(const char *sourceFile)
{
if (!sourceFile)
return "";
std::string p(sourceFile);
const size_t lastSlash = p.find_last_of('/');
if (lastSlash == std::string::npos)
return "";
p.erase(lastSlash);
const size_t prevSlash = p.find_last_of('/');
return prevSlash == std::string::npos ? p : p.substr(prevSlash + 1);
}
struct StateWatch {
bool resolved = false;
bool active = false;
std::string root;
std::string report;
std::map<std::string, uint64_t> previous;
};
StateWatch &watch()
{
static StateWatch w;
return w;
}
} // namespace
void testStateCheckpoint(const char *testName, const char *sourceFile)
{
StateWatch &w = watch();
if (!w.resolved) {
w.resolved = true;
const char *report = getenv("MESHTASTIC_TEST_STATE_REPORT");
const char *home = getenv("HOME");
// No report path means nobody asked: a bare `pio test` behaves exactly as before.
w.active = report && *report && home && *home;
if (w.active) {
w.report = report;
w.root = home;
}
}
if (!w.active)
return;
std::map<std::string, uint64_t> current;
walk(w.root, "", current);
// The priming call from initializeTestEnvironment() has no test to attribute to.
if (testName) {
const std::string suite = suiteFromPath(sourceFile);
FILE *out = fopen(w.report.c_str(), "a");
if (out) {
for (const auto &entry : current) {
auto prior = w.previous.find(entry.first);
if (prior == w.previous.end())
fprintf(out, "%s\t%s\tadded\t%s\n", suite.c_str(), testName, entry.first.c_str());
else if (prior->second != entry.second)
fprintf(out, "%s\t%s\tmodified\t%s\n", suite.c_str(), testName, entry.first.c_str());
}
for (const auto &entry : w.previous) {
if (current.find(entry.first) == current.end())
fprintf(out, "%s\t%s\tremoved\t%s\n", suite.c_str(), testName, entry.first.c_str());
}
fclose(out);
}
}
w.previous.swap(current);
}
#endif