Files
HaloKeymind/test/test_usb_logging_client_activity.py
T

196 lines
8.2 KiB
Python

"""Exercise the real RAM-only USB stats lease and bounded CDC input parser."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
MOCKS = ROOT / 'test' / 'fixtures' / 'usb_logging_watchdog' / 'mocks'
HARNESS = r'''
#include <Arduino.h>
#include <helpers/UsbLoggingClientActivity.h>
#include <cassert>
#include <cstdio>
#include <initializer_list>
#include <string>
using Parser = mesh::UsbLoggingStatsLineParser;
constexpr uint32_t Lease = mesh::USB_LOGGING_CLIENT_LEASE_MS;
static unsigned consume(Parser& parser, const std::string& input) {
unsigned matched = 0;
for (const char byte : input) if (parser.consume(byte)) ++matched;
return matched;
}
static void check_exact_stats_command_recognition() {
assert(Lease == 15U * 60U * 1000U);
assert(!mesh::isUsbLoggingStatsCommand(nullptr));
for (const char* command : {"stats-core", "stats-radio", "stats-packets",
" \tstats-core\t ", "stats-radio\t", " stats-packets"}) {
assert(mesh::isUsbLoggingStatsCommand(command));
}
for (const char* command : {"", " ", "stats", "stats-core2", "stats-radio-diag",
"stats-packets suffix", "STATS-CORE", "get name",
"get usb.watchdog", "usb.logger heartbeat", "stats-core\r",
"stats-core\n", "stats-core\b", "stats-cor", "stats-core;"}) {
assert(!mesh::isUsbLoggingStatsCommand(command));
}
}
static void check_default_expiry_and_renewal() {
mock_watchdog_millis = 0;
mesh::clearUsbLoggingClientActivity();
assert(!mesh::isUsbLoggingClientActive());
for (const char* command : {"stats-core", "stats-radio", "stats-packets"}) {
mock_watchdog_millis = 100;
mesh::noteUsbLoggingStatsCommand(command);
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis = 100 + Lease - 1;
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis = 100 + Lease;
assert(!mesh::isUsbLoggingClientActive());
assert(!mesh::isUsbLoggingClientActive());
}
mesh::clearUsbLoggingClientActivity();
for (uint32_t cycle = 0; cycle != 100; ++cycle) {
mock_watchdog_millis = cycle * 300000U;
mesh::noteUsbLoggingStatsCommand("stats-core");
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis += 300000U - 1;
assert(mesh::isUsbLoggingClientActive());
}
const auto last = 99U * 300000U;
mock_watchdog_millis = last + Lease - 1;
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis = last + Lease;
assert(!mesh::isUsbLoggingClientActive());
// Expiration is latched. Multiple millis rollovers cannot revive an old poll.
for (uint64_t day = 1; day <= 150; ++day) {
mock_watchdog_millis = uint32_t(uint64_t(last) + day * 86400000ULL);
assert(!mesh::isUsbLoggingClientActive());
}
mock_watchdog_millis = last; // Exactly the old uint32 clock value after wrap.
assert(!mesh::isUsbLoggingClientActive());
}
static void check_invalid_input_never_extends_or_starts_a_lease() {
for (const char* command : {"stats-core extra", "stats-radio-diag", "ver", "",
"usb.logger heartbeat", "stats-packets\n"}) {
mock_watchdog_millis = 1000;
mesh::clearUsbLoggingClientActivity();
mesh::noteUsbLoggingStatsCommand(command);
assert(!mesh::isUsbLoggingClientActive());
mesh::noteUsbLoggingStatsCommand("stats-radio");
mock_watchdog_millis = 1000 + Lease - 1;
mesh::noteUsbLoggingStatsCommand(command);
assert(mesh::isUsbLoggingClientActive());
++mock_watchdog_millis;
assert(!mesh::isUsbLoggingClientActive());
}
mesh::noteUsbLoggingStatsCommand(nullptr);
assert(!mesh::isUsbLoggingClientActive());
}
static void check_session_clear_and_clock_wrap() {
mock_watchdog_millis = UINT32_MAX - 1000;
mesh::clearUsbLoggingClientActivity();
mesh::noteUsbLoggingStatsCommand(" \tstats-packets\t ");
const uint32_t started = mock_watchdog_millis;
mock_watchdog_millis = started + Lease - 1;
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis = started + Lease;
assert(!mesh::isUsbLoggingClientActive());
mesh::noteUsbLoggingStatsCommand("stats-core");
assert(mesh::isUsbLoggingClientActive());
mesh::clearUsbLoggingClientActivity();
assert(!mesh::isUsbLoggingClientActive());
mesh::noteUsbLoggingStatsCommand("stats-radio");
assert(mesh::isUsbLoggingClientActive());
for (unsigned clear = 0; clear != 10000; ++clear) {
mesh::clearUsbLoggingClientActivity();
assert(!mesh::isUsbLoggingClientActive());
}
mesh::noteUsbLoggingStatsCommand("stats-core");
assert(mesh::isUsbLoggingClientActive());
// Deadline zero is reserved. This rare rollover expires only one ms early.
mesh::clearUsbLoggingClientActivity();
mock_watchdog_millis = uint32_t(0U - Lease);
const uint32_t zero_deadline_start = mock_watchdog_millis;
mesh::noteUsbLoggingStatsCommand("stats-core");
mock_watchdog_millis = zero_deadline_start + Lease - 2;
assert(mesh::isUsbLoggingClientActive());
mock_watchdog_millis = zero_deadline_start + Lease - 1;
assert(!mesh::isUsbLoggingClientActive());
mock_watchdog_millis = 0;
assert(!mesh::isUsbLoggingClientActive());
}
static void check_bounded_logging_endpoint_line_parser() {
Parser parser;
assert(consume(parser, "stats-core\r\n") == 1);
assert(consume(parser, "stats-radio\n") == 1);
assert(consume(parser, "stats-packets\r") == 1);
assert(consume(parser, "stats-core") == 0);
assert(consume(parser, "\n") == 1);
assert(consume(parser, " \tstats-radio \t\r\n") == 1);
assert(consume(parser, "stats-radio-diag\r\n") == 0);
assert(consume(parser, "stats-packets suffix\r\n") == 0);
assert(consume(parser, "\r\n\r\n") == 0);
assert(consume(parser, "stats-core") == 0);
parser.reset();
assert(consume(parser, "\n") == 0);
assert(consume(parser, std::string(21, ' ') + "stats-core\n") == 1); // 31 bytes.
assert(consume(parser, std::string(22, ' ') + "stats-core\n") == 0); // 32 bytes.
assert(consume(parser, std::string(1048576, 'x') + "stats-core\n") == 0);
assert(consume(parser, "stats-core\n") == 1); // Recover after rejected line.
assert(consume(parser, std::string("stats-core\0suffix\n", 18)) == 0);
assert(consume(parser, "stats-core\n") == 1);
assert(consume(parser, std::string(1, '\0') + "stats-core\n") == 0);
assert(consume(parser, "stats-core\n") == 1);
assert(consume(parser, "stats-core\b\n") == 0);
assert(consume(parser, "stats-core\x7f\n") == 0);
}
int main() {
check_exact_stats_command_recognition();
check_default_expiry_and_renewal();
check_invalid_input_never_extends_or_starts_a_lease();
check_session_clear_and_clock_wrap();
check_bounded_logging_endpoint_line_parser();
puts("USB stats client activity lease passed");
}
'''
class UsbLoggingClientActivityTest(unittest.TestCase):
def test_actual_client_activity_and_parser(self):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if compiler is None:
self.skipTest('a host C++17 compiler is required')
sanitizer_flags = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
with tempfile.TemporaryDirectory(prefix='usb-logging-client-') as directory:
source = Path(directory) / 'test.cpp'
binary = Path(directory) / 'test.exe'
source.write_text(HARNESS, encoding='utf-8')
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', '-DARDUINO',
'-DESP32', '-DARDUINO_USB_MODE=0', '-DARDUINO_USB_CDC_ON_BOOT=1',
*sanitizer_flags, '-I' + str(MOCKS), '-I' + str(ROOT / 'src'),
str(source), str(ROOT / 'src' / 'helpers' / 'UsbLoggingClientActivity.cpp'),
'-o', str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stderr)
self.assertIn('USB stats client activity lease passed', tested.stdout)
if __name__ == '__main__':
unittest.main()