203 lines
11 KiB
Python
203 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Qualify the capture-only startup hook against Arduino3.3.11's actual ISR."""
|
|
import hashlib
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import test_hwcdc_startup_session as startup
|
|
import test_hwcdc_tx_backport as backport
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CORE = ROOT / "test/fixtures/hwcdc_3311"
|
|
FIX = backport.module
|
|
RAW = (CORE / "HWCDC.cpp").read_text()
|
|
PATCHED = FIX.patched_hwcdc_startup_source(RAW)
|
|
|
|
|
|
def harness(patched=PATCHED, **controls):
|
|
state, functions, owner = startup.application_sources(**controls)
|
|
# Reuse only the peripheral boundary. All driver methods below come from
|
|
# the reviewed 3.x implementation with its capture-only startup patch.
|
|
prefix = backport.HARNESS.split("@SUPPORT@", 1)[0]
|
|
prefix = prefix.replace("static std::vector<unsigned> event_lengths;",
|
|
"static std::vector<unsigned> event_lengths;\n"
|
|
"static std::deque<int> queued_events;")
|
|
prefix = prefix.replace(
|
|
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);",
|
|
" queued_events.push_back(event);\n"
|
|
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);")
|
|
has_stash = "static volatile size_t tx_stash_len" in patched
|
|
fields = "\n".join(re.search(r"^static [^\n]*\b" + name + r"[^\n]*;",
|
|
patched, re.MULTILINE)[0]
|
|
for name in ("tx_stash_buf", "tx_stash_len", "hw_cdc_tx_mux")) if has_stash else ""
|
|
signatures = (
|
|
"static inline void hw_cdc_enable_tx_intr(void)",
|
|
"static inline void hw_cdc_enable_tx_intr_from_isr(void)",
|
|
"static inline void hw_cdc_disable_tx_intr_from_isr(void)",
|
|
"static inline void hw_cdc_clear_tx_stash(void)",
|
|
"bool HWCDC::isCDC_Connected()",
|
|
"static void hw_cdc_isr_handler(",
|
|
)
|
|
if not has_stash:
|
|
# The 3.1.3 driver has no TX stash/mux helpers and directly updates
|
|
# interrupts. Do not invent the newer driver's locking guarantees.
|
|
signatures = signatures[-2:]
|
|
prefix = prefix.replace("assert(critical_depth);", "")
|
|
driver = "\n".join(backport.body(patched, signature) for signature in signatures)
|
|
service = r'''
|
|
static void pulse(unsigned limit=64) {
|
|
fifo_limit=limit; intr_status=USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY;
|
|
hw_cdc_isr_handler(nullptr);
|
|
}
|
|
static void reset() {
|
|
ring=Ring{}; staged.clear(); event_lengths.clear(); queued_events.clear();
|
|
@RESET_STASH@ irq_enabled=true; plugged=false; connected=false;
|
|
before_empty_return=nullptr; flushes=0;
|
|
}
|
|
'''
|
|
service = service.replace("@RESET_STASH@", "tx_stash_len=0;" if has_stash else "")
|
|
extra = startup.EXTRA.replace("#define MESH_HWCDC_PINNED_TX_BACKPORT 1",
|
|
"#define MESH_HWCDC_PINNED_TX_BACKPORT 0")
|
|
extra = extra.replace("mesh_hwcdc_tx_stash_len", "tx_stash_len")
|
|
extra = extra.replace(" && mesh_hwcdc_fifo_pending", "")
|
|
extra = extra.replace(" && !mesh_hwcdc_fifo_pending", "")
|
|
extra = extra.replace(" && mesh_hwcdc_tx_allowed", "")
|
|
if not has_stash:
|
|
# 3.1.3 does not retain partially accepted FIFO suffixes. Exercise its
|
|
# real boot TX event with a fully accepted packet, then enumeration.
|
|
extra = extra.replace(
|
|
"// Real ISR stages a partial boot diagnostic before the host enumerates.",
|
|
"// Real 3.1.3 ISR stages a boot diagnostic before the host enumerates.")
|
|
extra = extra.replace("pulse(4); assert(tx_stash_len != 0);",
|
|
"pulse(); assert(staged.size() == 11 && ring.data.empty());")
|
|
extra = extra.replace(" assert(tx_stash_len == 0);\n", "")
|
|
extra = extra.replace("@STARTUP_STATE@", state).replace(
|
|
"@FUNCTIONS@", functions).replace("@OWNER_RESET_HOOK@", owner)
|
|
return prefix + FIX.HWCDC_STARTUP_HOOK + fields + "\n" + driver + service + extra
|
|
|
|
|
|
class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests):
|
|
# Run the exact same client/session assertions and negative controls with
|
|
# the actual 3.3.11 ISR and non-backport application cleanup branches.
|
|
core, raw, patched, version = CORE, RAW, PATCHED, (3, 3, 11)
|
|
|
|
def run_case(self, case, *, expect_success=True, **controls):
|
|
with tempfile.TemporaryDirectory(prefix="hwcdc-3311-startup-") as directory:
|
|
directory = Path(directory)
|
|
cpp, binary = directory / "startup.cpp", directory / "startup"
|
|
cpp.write_text(harness(self.patched, **controls), encoding="ascii")
|
|
compiled = subprocess.run([
|
|
"g++", "-std=c++17", "-pthread", "-Wall", "-Wextra",
|
|
"-Wno-unused-parameter", "-Wno-sign-compare",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie", "-I" + str(ROOT / "src"),
|
|
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
|
result = subprocess.run([str(binary), case], capture_output=True,
|
|
text=True, timeout=10)
|
|
if expect_success:
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
else:
|
|
self.assertNotEqual(result.returncode, 0, "Negative control did not fail")
|
|
|
|
def test_only_reset_event_posting_and_hook_declaration_change(self):
|
|
raw_pin, patched_pin = FIX.HWCDC_STARTUP_PINS[self.version]
|
|
self.assertEqual(hashlib.sha256(self.raw.encode()).hexdigest(), raw_pin)
|
|
self.assertEqual(hashlib.sha256(self.patched.encode()).hexdigest(), patched_pin)
|
|
self.assertEqual(self.patched.count(FIX.HWCDC_STARTUP_HOOK), 1)
|
|
self.assertEqual(self.patched.count(FIX.HWCDC_RESET_CAPTURE), 1)
|
|
reverted = self.patched.replace(FIX.HWCDC_STARTUP_HOOK, "", 1).replace(
|
|
FIX.HWCDC_RESET_CAPTURE, FIX.HWCDC_RESET_POST, 1)
|
|
self.assertEqual(reverted, self.raw, "Unrelated driver code changed")
|
|
patch = lambda source: FIX.patched_hwcdc_startup_source(source, version=self.version)
|
|
self.assertEqual(patch(self.patched), self.patched)
|
|
self.assertEqual(patch(self.raw.replace("\n", "\r\n")), self.patched)
|
|
for changed in (self.raw + "\n", self.patched + "\n",
|
|
self.raw.replace("connected = false;", "connected = true;", 1)):
|
|
with self.subTest(changed=hashlib.sha256(changed.encode()).hexdigest()):
|
|
with self.assertRaises(RuntimeError):
|
|
patch(changed)
|
|
|
|
def test_build_local_patch_has_exact_version_and_mode_scope(self):
|
|
class Node:
|
|
def __init__(self, path): self.path = path
|
|
def srcnode(self): return self
|
|
def get_abspath(self): return str(self.path)
|
|
class Environment(dict):
|
|
def __init__(self, definitions, build):
|
|
super().__init__(CPPDEFINES=definitions)
|
|
self.build, self.paths = build, []
|
|
def subst(self, value):
|
|
assert value == "$BUILD_DIR"
|
|
return str(self.build)
|
|
def File(self, path): return path
|
|
def AppendUnique(self, **kwargs): self.paths += kwargs["CPPPATH"]
|
|
with tempfile.TemporaryDirectory(prefix="hwcdc-3311-source-") as directory:
|
|
directory = Path(directory)
|
|
sdk = directory / "sdk"; sdk.mkdir()
|
|
path, version = sdk / "HWCDC.cpp", sdk / "esp_arduino_version.h"
|
|
path.write_text(self.raw, encoding="ascii")
|
|
version.write_text((self.core / "esp_arduino_version.h").read_text(), encoding="ascii")
|
|
node = Node(path)
|
|
for definitions in ([], [("ARDUINO_USB_CDC_ON_BOOT", 0), ("ARDUINO_USB_MODE", 1)],
|
|
[("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 0)]):
|
|
env = Environment(definitions, directory / "build")
|
|
self.assertIs(FIX.replace_hwcdc_source(env, node), node)
|
|
self.assertFalse(env.build.exists())
|
|
env = Environment([("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 1)],
|
|
directory / "build")
|
|
result = Path(FIX.replace_hwcdc_source(env, node))
|
|
self.assertEqual(result.read_text(), self.patched)
|
|
self.assertEqual(path.read_text(), self.raw)
|
|
self.assertEqual(env.paths, [str(sdk)])
|
|
before = result.stat().st_mtime_ns
|
|
FIX.replace_hwcdc_source(env, node)
|
|
self.assertEqual(result.stat().st_mtime_ns, before)
|
|
path.write_text(self.raw + "\n", encoding="ascii")
|
|
with self.assertRaises(RuntimeError):
|
|
FIX.replace_hwcdc_source(env, node)
|
|
# Reviewed versions cannot borrow another version's source pin.
|
|
path.write_text(self.raw, encoding="ascii")
|
|
other = (3, 1, 3) if self.version == (3, 3, 11) else (3, 3, 11)
|
|
version.write_text("\n".join("#define ESP_ARDUINO_VERSION_" + name + " " + str(value)
|
|
for name, value in zip(("MAJOR", "MINOR", "PATCH"), other)) + "\n",
|
|
encoding="ascii")
|
|
with self.assertRaises(RuntimeError):
|
|
FIX.replace_hwcdc_source(env, node)
|
|
# An unreviewed framework version keeps its existing driver.
|
|
version.write_text("#define ESP_ARDUINO_VERSION_MAJOR 3\n"
|
|
"#define ESP_ARDUINO_VERSION_MINOR 3\n"
|
|
"#define ESP_ARDUINO_VERSION_PATCH 12\n", encoding="ascii")
|
|
self.assertIs(FIX.replace_hwcdc_source(env, node), node)
|
|
|
|
def test_shared_predicate_is_exported_without_the_217_tx_backport(self):
|
|
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
|
hook = source.index('extern "C" bool meshEsp32HwcdcShouldReportBusReset()')
|
|
start = source.rfind("#if ", 0, hook)
|
|
end = source.index("#endif", hook) + len("#endif")
|
|
block = source[start:end]
|
|
for guard in (0, 1):
|
|
code = "#include <atomic>\n#include <cassert>\n"
|
|
code += "#define MESH_ESP32_HWCDC_SESSION_GUARD " + str(guard) + "\n"
|
|
code += "#define MESH_HWCDC_PINNED_TX_BACKPORT 0\n"
|
|
code += "namespace mesh { static std::atomic<bool> esp32_hwcdc_startup_pending{true}; }\n"
|
|
code += block + "\nint main() {\n#if MESH_ESP32_HWCDC_SESSION_GUARD\n"
|
|
code += "assert(!meshEsp32HwcdcShouldReportBusReset());\n"
|
|
code += "mesh::esp32_hwcdc_startup_pending.store(false);\n"
|
|
code += "assert(meshEsp32HwcdcShouldReportBusReset());\n#endif\n}\n"
|
|
with self.subTest(guard=guard), tempfile.TemporaryDirectory() as directory:
|
|
cpp, binary = Path(directory) / "export.cpp", Path(directory) / "export"
|
|
cpp.write_text(code, encoding="ascii")
|
|
result = subprocess.run(["g++", "-std=c++17", str(cpp), "-o", str(binary)],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|