mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 23:13:19 +00:00
185 lines
9.2 KiB
Python
185 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute Sensor subscription parsing and push admission with real LPP helpers."""
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE = ROOT / 'examples/simple_sensor/SensorMesh.cpp'
|
|
HELPER = ROOT / 'src/helpers/sensors/LPPDataHelpers.h'
|
|
FIXTURE = ROOT / 'test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp'
|
|
|
|
|
|
def production_methods(source):
|
|
methods = extract_braced(source, 'static float findTelemValue(') + '\n'
|
|
methods += extract_braced(source, 'bool SensorMesh::telemHasChanged(') + '\n'
|
|
if 'void SensorMesh::snapshotTelemetry(' in source:
|
|
methods += extract_braced(source, 'void SensorMesh::snapshotTelemetry(') + '\n'
|
|
handler = extract_braced(source, 'uint8_t SensorMesh::handleRequest(')
|
|
prefix = handler[:handler.index(' if (req_type == REQ_TYPE_GET_TELEMETRY_DATA')]
|
|
methods += prefix + extract_braced(handler, 'if (req_type == REQ_TYPE_SUBSCRIBE')
|
|
methods += '\nreturn 0;\n}\n'
|
|
loop = source[source.index(' uint32_t curr = getRTCClock()->getCurrentTime();', source.index('void SensorMesh::loop()')):]
|
|
constructor = extract_braced(source, 'SensorMesh::SensorMesh(')
|
|
capacity = re.search(r'telemetry\(([^)]+)\)', constructor).group(1)
|
|
methods += 'SensorMesh::SensorMesh() : telemetry(' + capacity + ') {}\n'
|
|
methods += 'void SensorMesh::pushSubscriptions() { uint32_t curr = 100;\n'
|
|
methods += extract_braced(loop, 'for (int i = 0; i < acl.getNumClients(); i++)') + '\n}\n'
|
|
packet = (ROOT / 'src/Packet.cpp').read_text()
|
|
methods += 'namespace mesh {\n' + extract_braced(packet, 'Packet::Packet()') + '\n'
|
|
methods += extract_braced(packet, 'bool Packet::isValidPathLen(') + '\n}\n'
|
|
return methods
|
|
|
|
|
|
class SensorSubscriptionTest(unittest.TestCase):
|
|
def execute(self, case, transform=None, helper_transform=None):
|
|
compiler = shutil.which('g++') or shutil.which('clang++')
|
|
self.assertIsNotNone(compiler, 'host C++17 compiler required')
|
|
source = SOURCE.read_text()
|
|
helper = HELPER.read_text()
|
|
if transform:
|
|
source = transform(source)
|
|
if helper_transform:
|
|
helper = helper_transform(helper)
|
|
with tempfile.TemporaryDirectory(prefix='sensor-subscription-') as directory:
|
|
work = Path(directory)
|
|
(work / 'production.inc').write_text(production_methods(source), encoding='ascii')
|
|
(work / 'LPPDataHelpers.h').write_text(helper, encoding='ascii')
|
|
binary = work / 'test'
|
|
args = [compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', '-Wno-unused-parameter',
|
|
'-I' + str(work), '-I' + str(ROOT / 'src'), str(FIXTURE), '-o', str(binary)]
|
|
if sys.platform.startswith('linux'):
|
|
args[1:1] = ['-fsanitize=address,undefined,float-cast-overflow', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
|
|
built = subprocess.run(args, capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
return subprocess.run([str(binary), case], capture_output=True, text=True, timeout=30)
|
|
|
|
def passed(self, case):
|
|
run = self.execute(case)
|
|
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
|
self.assertIn('Sensor subscription checks passed', run.stdout)
|
|
|
|
def test_supported_scalars_and_humidity_wire_encoding(self):
|
|
self.passed('scalar')
|
|
|
|
def test_unsupported_types_truncation_and_existing_subscription_preserved(self):
|
|
self.passed('input')
|
|
|
|
def test_allocation_and_queue_failures_keep_delta_available_for_retry(self):
|
|
self.passed('admission')
|
|
|
|
def test_subscription_collection_and_push_fit_host_packet_budget(self):
|
|
self.passed('capacity')
|
|
|
|
def test_scalar_writer_rejections_leave_buffer_and_length_untouched(self):
|
|
self.passed('writer')
|
|
|
|
def test_scalar_width_sign_nonfinite_and_uint32_round_trip(self):
|
|
self.passed('conversion')
|
|
|
|
def test_old_acceptance_of_vector_delta_records_is_rejected(self):
|
|
def old(source):
|
|
guard = 'LPPData::isScalarType(type)\n && item_len <= min_deltas_len - i'
|
|
self.assertIn(guard, source)
|
|
return source.replace(guard, 'item_len <= min_deltas_len - i', 1)
|
|
self.assertNotEqual(self.execute('input', old).returncode, 0)
|
|
|
|
def test_old_invalid_request_overwrites_existing_push_tag(self):
|
|
def old(source):
|
|
marker = ' if (r && valid_deltas) {\n memcpy(&from->extra.sensor.push_tag, &payload[0], 4);'
|
|
self.assertIn(marker, source)
|
|
source = source.replace(marker, ' if (r && valid_deltas) {', 1)
|
|
marker = ' uint16_t timeout_secs;'
|
|
return source.replace(marker, ' memcpy(&from->extra.sensor.push_tag, &payload[0], 4);\n' + marker, 1)
|
|
self.assertNotEqual(self.execute('input', old).returncode, 0)
|
|
|
|
def test_old_early_threshold_snapshot_is_rejected(self):
|
|
def old(source):
|
|
begin = source.index('bool SensorMesh::telemHasChanged(')
|
|
end = source.index('void SensorMesh::snapshotTelemetry(', begin)
|
|
before = source[begin:end]
|
|
self.assertIn(' return changed;', before)
|
|
altered = before.replace(' return changed;', ' if (changed) snapshotTelemetry(c);\n return changed;', 1)
|
|
return source[:begin] + altered + source[end:]
|
|
self.assertNotEqual(self.execute('admission', old).returncode, 0)
|
|
|
|
def test_old_humidity_width_and_multiplier_are_rejected(self):
|
|
def width(helper):
|
|
marker = ' case LPP_ALTITUDE:\n case LPP_VOLTAGE:'
|
|
self.assertIn(marker, helper)
|
|
return helper.replace(marker, ' case LPP_RELATIVE_HUMIDITY:\n' + marker, 1)
|
|
def multiplier(helper):
|
|
marker = ' case LPP_RELATIVE_HUMIDITY:\n return 2;'
|
|
self.assertIn(marker, helper)
|
|
return helper.replace(marker, ' case LPP_RELATIVE_HUMIDITY:\n return 10;', 1)
|
|
for old in (width, multiplier):
|
|
with self.subTest(old=old.__name__):
|
|
self.assertNotEqual(self.execute('scalar', helper_transform=old).returncode, 0)
|
|
|
|
def test_old_unbounded_subscription_collection_is_rejected(self):
|
|
def old(source):
|
|
marker = ' telemetry(mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8)'
|
|
self.assertIn(marker, source)
|
|
return source.replace(marker, ' telemetry(MAX_PACKET_PAYLOAD - 4)', 1)
|
|
self.assertNotEqual(self.execute('capacity', old).returncode, 0)
|
|
|
|
def test_old_push_allows_oversized_response_attempt(self):
|
|
def old(source):
|
|
marker = ' if (telemetry_len > mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8) continue;'
|
|
self.assertIn(marker, source)
|
|
return source.replace(marker, ' if (telemetry_len > sizeof(reply_data) - 8) continue;', 1)
|
|
self.assertNotEqual(self.execute('capacity', old).returncode, 0)
|
|
|
|
def test_old_signed_four_byte_mask_is_rejected_by_sanitizer(self):
|
|
def old(helper):
|
|
marker = 'size == sizeof(uint32_t)\n ? UINT32_MAX : (uint32_t(1) << (size * 8)) - 1'
|
|
self.assertIn(marker, helper)
|
|
return helper.replace(marker, '(1 << (size * 8)) - 1', 1)
|
|
self.assertNotEqual(self.execute('conversion', helper_transform=old).returncode, 0)
|
|
|
|
def test_old_writer_commits_incomplete_header_on_encoding_failure(self):
|
|
def old(helper):
|
|
begin = helper.index(' if (LPPData::isScalarType(type)', helper.index('bool writeData('))
|
|
end = helper.index(' return false;', begin)
|
|
original = ''' if (_len + 2 + sz <= _max_len) {
|
|
_buf[_len++] = channel;
|
|
_buf[_len++] = type;
|
|
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
|
|
return true;
|
|
}
|
|
'''
|
|
return helper[:begin] + original + helper[end:]
|
|
self.assertNotEqual(self.execute('writer', helper_transform=old).returncode, 0)
|
|
|
|
def test_old_unguarded_float_cast_is_rejected_by_sanitizer(self):
|
|
def old(helper):
|
|
begin = helper.index(' if (size == 0', helper.index('static uint8_t putFloat('))
|
|
end = helper.index(' // add bytes (MSB first)', begin)
|
|
original = ''' bool sign = value < 0;
|
|
if (sign) value = -value;
|
|
uint32_t v = value * multiplier;
|
|
if (is_signed && sign) {
|
|
const uint32_t mask = size == 4 ? UINT32_MAX : (uint32_t(1) << (size * 8)) - 1;
|
|
v = (uint32_t(0) - v) & mask;
|
|
}
|
|
'''
|
|
return helper[:begin] + original + helper[end:]
|
|
self.assertNotEqual(self.execute('conversion', helper_transform=old).returncode, 0)
|
|
|
|
def test_header_and_ci_wiring(self):
|
|
self.assertIn(' electroniccats/CayenneLPP @ 1.6.1', (ROOT / 'platformio.ini').read_text())
|
|
self.assertIn(' void snapshotTelemetry(ClientInfo* c);',
|
|
(ROOT / 'examples/simple_sensor/SensorMesh.h').read_text())
|
|
self.assertIn(' python3 -B test/test_sensor_subscriptions.py -v\n',
|
|
(ROOT / '.github/workflows/run-unit-tests.yml').read_text())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|