Files
MeshCore/test/test_sh1106_memory.py
T

325 lines
13 KiB
Python

#!/usr/bin/env python3
"""Exercise the real SH1106 driver with bounded vendor allocation stand-ins.
The stand-in follows pinned GrayOLED allocation order: one reusable framebuffer
and a newly allocated I2C adapter on each vendor begin, including failed probes.
This checks ownership, address selection and rendering, not physical I2C timing.
"""
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
DRIVER = ROOT / "src/helpers/ui/SH1106Display.cpp"
HEADER = ROOT / "src/helpers/ui/SH1106Display.h"
WIRE = r'''
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
struct TwoWire {
bool present[128] = {};
uint8_t probes[16] = {};
unsigned probe_count = 0, begins = 0;
uint8_t address = 0;
void begin() { ++begins; }
void beginTransmission(uint8_t value) {
address = value;
if (probe_count < sizeof(probes)) probes[probe_count++] = value;
}
uint8_t endTransmission() { return present[address] ? 0 : 4; }
void clearProbes() { probe_count = 0; }
};
inline TwoWire Wire;
'''
VENDOR = r'''
#pragma once
#include <Wire.h>
#include <cstdlib>
#include <cstring>
#include <new>
#define SH110X_BLACK 0
#define SH110X_WHITE 1
#define SH110X_DISPLAYON 0xAF
#define SH110X_DISPLAYOFF 0xAE
struct PanelState {
bool framebuffer_allowed = true, adapter_begin_allowed = true;
unsigned buffers = 0, buffer_allocations = 0, buffer_frees = 0, peak_buffers = 0;
unsigned adapters = 0, adapter_allocations = 0, adapter_frees = 0, peak_adapters = 0;
unsigned begins = 0, clears = 0, frames = 0, on_commands = 0, off_commands = 0;
unsigned fills = 0, rectangles = 0, bitmaps = 0, prints = 0;
uint8_t address = 0;
bool reset = false, cp437_enabled = false;
int width = 0, height = 0, text_size = 0, cursor_x = 0, cursor_y = 0;
uint16_t text_color = 0, drawing_color = 0;
uint8_t* buffer = nullptr;
char text[64] = {};
};
inline PanelState panel;
class Adafruit_I2CDevice {
uint8_t address;
TwoWire* wire;
public:
Adafruit_I2CDevice(uint8_t value, TwoWire* bus) : address(value), wire(bus) {}
static void* operator new(std::size_t bytes) {
void* result = std::malloc(bytes);
if (!result) throw std::bad_alloc();
++panel.adapters;
++panel.adapter_allocations;
if (panel.adapters > panel.peak_adapters) panel.peak_adapters = panel.adapters;
return result;
}
static void operator delete(void* pointer) noexcept {
assert(panel.adapters > 0);
--panel.adapters;
++panel.adapter_frees;
std::free(pointer);
}
bool begin() {
wire->begin();
wire->beginTransmission(address);
return wire->endTransmission() == 0 && panel.adapter_begin_allowed;
}
};
class Adafruit_SH1106G {
uint16_t width, height;
TwoWire* wire;
protected:
Adafruit_I2CDevice* i2c_dev = nullptr;
uint8_t* buffer = nullptr;
public:
Adafruit_SH1106G(uint16_t w, uint16_t h, TwoWire* bus, int)
: width(w), height(h), wire(bus) {
panel.width = w; panel.height = h;
}
~Adafruit_SH1106G() {
delete i2c_dev;
if (buffer) {
assert(panel.buffers > 0);
--panel.buffers; ++panel.buffer_frees;
std::free(buffer);
panel.buffer = nullptr;
}
}
bool begin(uint8_t address, bool reset) {
++panel.begins; panel.address = address; panel.reset = reset;
if (!buffer) {
if (!panel.framebuffer_allowed) return false;
buffer = static_cast<uint8_t*>(std::malloc(width * ((height + 7) / 8)));
assert(buffer);
++panel.buffers; ++panel.buffer_allocations;
if (panel.buffers > panel.peak_buffers) panel.peak_buffers = panel.buffers;
panel.buffer = buffer;
}
// Deliberately match GrayOLED: the wrapper must release the old adapter.
i2c_dev = new Adafruit_I2CDevice(address, wire);
if (!i2c_dev->begin()) return false;
clearDisplay();
return true;
}
void clearDisplay() { ++panel.clears; std::memset(buffer, 0, width * ((height + 7) / 8)); }
void display() { ++panel.frames; }
void oled_command(uint8_t command) {
if (command == SH110X_DISPLAYON) ++panel.on_commands;
if (command == SH110X_DISPLAYOFF) ++panel.off_commands;
}
void setTextColor(uint16_t color) { panel.text_color = color; }
void setTextSize(int size) { panel.text_size = size; }
void cp437(bool enabled) { panel.cp437_enabled = enabled; }
void setCursor(int x, int y) { panel.cursor_x = x; panel.cursor_y = y; }
void print(const char* text) {
++panel.prints;
std::strncpy(panel.text, text, sizeof(panel.text) - 1);
}
void fillRect(int, int, int, int, uint16_t color) { ++panel.fills; panel.drawing_color = color; }
void drawRect(int, int, int, int, uint16_t color) { ++panel.rectangles; panel.drawing_color = color; }
void drawBitmap(int, int, const uint8_t* bits, int w, int h, uint16_t color) {
assert(bits && w > 0 && h > 0); ++panel.bitmaps; panel.drawing_color = color;
}
void getTextBounds(const char* text, int, int, int16_t* x, int16_t* y,
uint16_t* w, uint16_t* h) {
*x = *y = 0; *w = std::strlen(text) * 6; *h = 8;
}
};
'''
HARNESS = r'''
#include "SH1106Display.h"
#include <cassert>
#include <cstdio>
#include <cstring>
static_assert(SH1106Display::DISPLAY_WIDTH == 128, "preserve every display column");
static_assert(SH1106Display::DISPLAY_HEIGHT == 64, "preserve every display row");
static_assert(SH1106Display::FRAMEBUFFER_BYTES == 1024, "preserve the monochrome framebuffer");
void reset() {
assert(panel.buffers == 0 && panel.adapters == 0);
panel = PanelState{}; Wire = TwoWire{};
}
void bounded() {
assert(panel.buffers <= 1 && panel.adapters <= 1);
assert(panel.peak_buffers <= 1 && panel.peak_adapters <= 1);
}
void destroyed() {
assert(panel.buffers == 0 && panel.adapters == 0);
assert(panel.buffer_allocations == panel.buffer_frees);
assert(panel.adapter_allocations == panel.adapter_frees);
}
int main() {
reset();
{
SH1106Display driver;
assert(driver.width() == 128 && driver.height() == 64 && !driver.isOn());
Wire.present[DISPLAY_ADDRESS] = true;
for (unsigned cycle = 0; cycle < 200; ++cycle) {
Wire.clearProbes();
assert(driver.begin());
assert(panel.address == DISPLAY_ADDRESS && panel.reset);
assert(Wire.probe_count == 2 && Wire.probes[0] == DISPLAY_ADDRESS);
assert(panel.buffer_allocations == 1 && panel.adapters == 1);
bounded();
}
uint8_t* framebuffer = panel.buffer;
driver.turnOn(); assert(driver.isOn() && panel.on_commands == 1);
driver.turnOff(); assert(!driver.isOn() && panel.off_commands == 1);
driver.turnOn(); assert(driver.isOn() && panel.on_commands == 2);
assert(panel.begins == 200 && panel.buffer == framebuffer);
driver.clear();
assert(panel.frames == 1);
driver.startFrame();
assert(panel.text_color == SH110X_WHITE && panel.text_size == 1 && panel.cp437_enabled);
driver.setTextSize(2); assert(panel.text_size == 2);
driver.setCursor(7, 11); assert(panel.cursor_x == 7 && panel.cursor_y == 11);
driver.print("same pixels"); assert(!std::strcmp(panel.text, "same pixels"));
driver.setColor(SH110X_BLACK);
driver.fillRect(1, 2, 3, 4); assert(panel.fills == 1 && panel.drawing_color == SH110X_BLACK);
driver.drawRect(2, 3, 4, 5); assert(panel.rectangles == 1 && panel.drawing_color == SH110X_BLACK);
driver.setColor(SH110X_WHITE);
const uint8_t bitmap[] = {0x80};
driver.drawXbm(1, 2, bitmap, 1, 1); assert(panel.bitmaps == 1 && panel.drawing_color == SH110X_WHITE);
assert(driver.getTextWidth("same") == 24);
driver.endFrame(); assert(panel.frames == 2);
assert(panel.buffer == framebuffer && panel.buffer_allocations == 1);
Wire.present[DISPLAY_ADDRESS] = false;
for (unsigned cycle = 0; cycle < 100; ++cycle) {
assert(!driver.begin()); bounded();
assert(panel.buffer == framebuffer && panel.buffer_allocations == 1);
assert(panel.adapters == 1 && !driver.isOn());
}
unsigned commands = panel.on_commands + panel.off_commands;
driver.turnOn(); driver.turnOff();
assert(!driver.isOn() && panel.on_commands + panel.off_commands == commands);
Wire.present[DISPLAY_ADDRESS] = true;
panel.adapter_begin_allowed = false;
assert(!driver.begin()); bounded();
assert(panel.adapters == 1 && panel.buffer == framebuffer);
panel.adapter_begin_allowed = true;
panel.framebuffer_allowed = false; // existing buffer must remain reusable
assert(driver.begin()); bounded();
driver.turnOn(); assert(driver.isOn());
assert(panel.buffer_allocations == 1 && panel.buffer == framebuffer);
Wire.present[DISPLAY_ADDRESS] = false;
#ifdef DISPLAY_ADDRESS_ALT
Wire.present[DISPLAY_ADDRESS_ALT] = true;
Wire.present[DISPLAY_ADDRESS ^ 1] = true;
Wire.clearProbes();
assert(driver.begin() && panel.address == DISPLAY_ADDRESS_ALT);
assert(Wire.probes[0] == DISPLAY_ADDRESS && Wire.probes[1] == DISPLAY_ADDRESS_ALT);
Wire.present[DISPLAY_ADDRESS_ALT] = false;
#endif
Wire.present[DISPLAY_ADDRESS ^ 1] = true;
Wire.clearProbes();
assert(driver.begin() && panel.address == (DISPLAY_ADDRESS ^ 1));
bounded();
}
destroyed();
reset();
{
SH1106Display driver;
Wire.present[DISPLAY_ADDRESS] = true;
panel.framebuffer_allowed = false;
assert(!driver.begin() && panel.buffers == 0 && panel.adapters == 0);
driver.turnOn(); assert(!driver.isOn());
panel.framebuffer_allowed = true;
assert(driver.begin()); bounded();
}
destroyed();
for (unsigned cycle = 0; cycle < 64; ++cycle) {
reset();
{
SH1106Display driver;
// Destruction must reclaim allocations even after failed initialization.
Wire.present[DISPLAY_ADDRESS] = (cycle & 1) != 0;
assert(driver.begin() == ((cycle & 1) != 0));
bounded();
}
destroyed();
}
std::puts("PASS: SH1106 bounded allocation, retry, addressing and rendering");
}
'''
class Sh1106MemoryTests(unittest.TestCase):
def compile_run(self, definitions=(), header=None, expect_failure=False):
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")
with tempfile.TemporaryDirectory(prefix="meshcore-sh1106-memory-") as directory:
work = Path(directory)
for name, contents in {
"Wire.h": WIRE,
"Adafruit_GFX.h": "#pragma once\n",
"Adafruit_SH110X.h": VENDOR,
"Adafruit_GrayOLED.h": '#pragma once\n#include "Adafruit_SH110X.h"\n',
"SH1106Display.h": HEADER.read_text() if header is None else header,
"SH1106Display.cpp": DRIVER.read_text(),
"test.cpp": HARNESS,
}.items():
(work / name).write_text(contents, encoding="ascii")
binary = work / "display"
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter",
"-DESP32_PLATFORM=1", *sanitizer, *definitions,
"-I", str(work), "-I", str(ROOT / "src/helpers/ui"),
str(work / "test.cpp"), str(work / "SH1106Display.cpp"),
"-o", str(binary),
], text=True, capture_output=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], text=True, capture_output=True, timeout=15)
if expect_failure:
self.assertNotEqual(checked.returncode, 0, "unbounded adapter allocation escaped the regression")
self.assertIn("Assertion", checked.stderr)
else:
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS: SH1106 bounded allocation, retry, addressing and rendering", checked.stdout)
def test_real_driver_bounds_success_failure_recovery_and_address_fallback(self):
for definitions in ((), ("-DDISPLAY_ADDRESS=0x3D",),
("-DDISPLAY_ADDRESS=0x3C", "-DDISPLAY_ADDRESS_ALT=0x3E")):
with self.subTest(definitions=definitions):
self.compile_run(definitions)
def test_missing_i2c_adapter_release_negative_control(self):
header = HEADER.read_text()
mutated, count = re.subn(r"delete\s+i2c_dev\s*;", "/* missing adapter release */", header)
self.assertEqual(count, 1, "production subclass must release its previous adapter")
self.compile_run(header=mutated, expect_failure=True)
if __name__ == "__main__":
unittest.main()