mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-16 08:30:04 -04:00
* Stop breaking TestUtil.cpp on Windows, dangit! * Update test/TestUtil.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
333 lines
11 KiB
C++
333 lines
11 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 "platform/portduino/PortduinoGlue.h"
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <dirent.h>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#if ARCH_PORTDUINO
|
|
// A test binary must not be reachable from the network. main.cpp's setup()/loop() are compiled out
|
|
// under PIO_UNIT_TESTING, so the phone API, MQTT and the web server are never started - but that is
|
|
// a property of today's guards, not something anything checks. A suite that pulled in a service
|
|
// which binds a port would otherwise open one on the developer's machine, silently, for the length
|
|
// of the run. Assert the absence instead of trusting it.
|
|
//
|
|
// Listening sockets only: an outbound connection is a different (and louder) problem, and gethostby*
|
|
// opens transient sockets that would make an any-socket check flap.
|
|
// Linux-only: this check reads /proc; MinGW-w64 has no readlink() for fd links.
|
|
// Linux CI covers the check; native Windows uses a no-op.
|
|
#ifdef _WIN32
|
|
static void assertNoListeningSockets() {}
|
|
#else
|
|
static void assertNoListeningSockets()
|
|
{
|
|
// Socket fds appear as "socket:[inode]"; a listening TCP row in /proc/self/net carries st 0A.
|
|
std::set<std::string> ours;
|
|
if (DIR *fds = opendir("/proc/self/fd")) {
|
|
while (struct dirent *e = readdir(fds)) {
|
|
char path[64], target[128];
|
|
snprintf(path, sizeof(path), "/proc/self/fd/%s", e->d_name);
|
|
ssize_t n = readlink(path, target, sizeof(target) - 1);
|
|
if (n <= 0)
|
|
continue;
|
|
target[n] = '\0';
|
|
unsigned long inode = 0;
|
|
if (sscanf(target, "socket:[%lu]", &inode) == 1)
|
|
ours.insert(std::to_string(inode));
|
|
}
|
|
closedir(fds);
|
|
}
|
|
if (ours.empty())
|
|
return;
|
|
|
|
std::string offenders;
|
|
for (const char *table : {"/proc/self/net/tcp", "/proc/self/net/tcp6"}) {
|
|
FILE *f = fopen(table, "r");
|
|
if (!f)
|
|
continue;
|
|
char line[512];
|
|
bool header = true;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (header) {
|
|
header = false;
|
|
continue;
|
|
}
|
|
// sl local_address rem_address st tx:rx tr:when retrnsmt uid timeout inode
|
|
char local[128] = {0};
|
|
unsigned st = 0, uid = 0;
|
|
unsigned long inode = 0;
|
|
if (sscanf(line, "%*d: %127s %*s %x %*s %*s %*s %u %*d %lu", local, &st, &uid, &inode) != 4)
|
|
continue;
|
|
if (st != 0x0A) // TCP_LISTEN
|
|
continue;
|
|
if (ours.count(std::to_string(inode)) == 0)
|
|
continue;
|
|
offenders += " ";
|
|
offenders += local;
|
|
}
|
|
fclose(f);
|
|
}
|
|
if (offenders.empty())
|
|
return;
|
|
|
|
// Before UNITY_BEGIN(), so there is no Unity failure to record - and a test binary that has
|
|
// opened a port is not a result worth collecting. Fail the suite outright and say why.
|
|
fprintf(stderr,
|
|
"FATAL: test binary is listening on%s\n"
|
|
"A unit-test run must not be reachable. Something started a network service - check what\n"
|
|
"the suite constructs, and whether it belongs behind main.cpp's PIO_UNIT_TESTING guard.\n",
|
|
offenders.c_str());
|
|
fflush(stderr);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#if ARCH_PORTDUINO
|
|
static bool environmentBaselined = false;
|
|
|
|
// -s is how the harness keeps a test run off the host's radio: it makes portduinoSetup() skip the
|
|
// /etc/meshtasticd/config.yaml search and return before GPIO/SPI init. That job is done by the time
|
|
// any of this runs, and the flag's only remaining readers are behaviour we do want under test -
|
|
// wouldEncryptWithPKC() disables PKC while it is set. Clear it so suites exercise the production
|
|
// encode path; the radio choice is already made and is not revisited.
|
|
static void baselineEnvironment()
|
|
{
|
|
portduino_config.force_simradio = false;
|
|
assertNoListeningSockets();
|
|
environmentBaselined = true;
|
|
}
|
|
#endif
|
|
|
|
void testAssertEnvironmentIntact(const char *testName)
|
|
{
|
|
#if ARCH_PORTDUINO
|
|
// Not every suite calls initializeTestEnvironment() - test_atak does not - so the baseline
|
|
// cannot live only there, or those suites run with PKC off and skip the socket check. Establish
|
|
// it at the first RUN_TEST for whoever has not, and hold it from then on.
|
|
if (!environmentBaselined) {
|
|
baselineEnvironment();
|
|
return;
|
|
}
|
|
|
|
// Per test, not once per suite: a service that binds a port is opened by the code under test,
|
|
// not by the harness, so checking only at startup would miss every case that starts one.
|
|
assertNoListeningSockets();
|
|
|
|
if (!portduino_config.force_simradio)
|
|
return;
|
|
|
|
// Hard exit rather than TEST_FAIL: this runs between tests, outside any Unity test frame, so
|
|
// there is no failure to longjmp into. Repairing the flag silently would be worse - it would
|
|
// leave the suite that broke it passing.
|
|
for (FILE *out : {stdout, stderr})
|
|
fprintf(out,
|
|
"FATAL: force_simradio was set back on before %s\n"
|
|
"PKC is disabled while it is set, so the encode path under test falls back to channel\n"
|
|
"crypto and every later case asserts the wrong thing. A test that needs simradio must\n"
|
|
"restore the flag before it returns.\n",
|
|
testName ? testName : "(unknown test)");
|
|
fflush(stderr);
|
|
exit(EXIT_FAILURE);
|
|
#else
|
|
(void)testName;
|
|
#endif
|
|
}
|
|
|
|
void initializeTestEnvironment()
|
|
{
|
|
concurrency::hasBeenSetup = true;
|
|
consoleInit();
|
|
#if ARCH_PORTDUINO
|
|
baselineEnvironment();
|
|
|
|
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
|