ESPSomfy-RTS/test/test_configrecords/test_configrecords.cpp
cjkas 92cd54f097 fix: review fixes for the shades.cfg records
- A group in a backup from before version 23 has no rolling code, and a
  restore wrote 0 to NVS for it.  It keeps the code NVS has.
- A restore reads each record into one that starts as the shade, group
  or settings it replaces, so what an older version lacks stays as it
  was (the update check below version 20 among them), as before.
- restoreFile and loadFile read the rooms, shades and groups through one
  readModel(); the settings, network and radio are applied by helpers.
- The shade's tilt is forgotten where the shades are written, not in the
  conversion to a record.
- rollingCodeAfterLoad moves to RollingCode.h and its tests to
  test_rollingcode.
- The topics a deleted shade or group clears are one list each
  (MqttTopics.h), tested against what is published.
- e2e: a restore of the shades brings back every shade and group, and
  the README has the rollback check by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:25:07 +01:00

850 lines
31 KiB
C++

#include <unity.h>
#include <fstream>
#include <sstream>
#include <string>
#include "ConfigRecords.h"
void setUp() {}
void tearDown() {}
// A file in memory.
class TextWriter : public ConfigWriter {
public:
std::string text;
bool writeBytes(const uint8_t *buf, size_t len) override { text.append(reinterpret_cast<const char *>(buf), len); return true; }
};
class TextReader : public ConfigReader {
public:
std::string text;
size_t pos = 0;
explicit TextReader(const std::string &t) : text(t) {}
bool readByte(uint8_t &val) override {
if(pos >= text.size()) return false;
val = static_cast<uint8_t>(text[pos++]);
return true;
}
uint32_t position() override { return static_cast<uint32_t>(pos); }
};
static config_header_t header(uint8_t version = SHADE_HDR_VER) {
config_header_t h;
h.version = version;
return h;
}
static shade_record_t aShade() {
shade_record_t rec;
rec.shadeId = 7;
rec.paired = true;
rec.shadeType = shade_types::blind;
rec.remoteAddress = 0x123456;
strcpy(rec.name, "Kitchen");
rec.tiltType = tilt_types::tiltmotor;
rec.proto = radio_proto::GP_Remote;
rec.bitLength = 80;
rec.upTime = 21000;
rec.downTime = 19500;
rec.tiltTime = 3200;
rec.stepSize = 250;
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) rec.linkedRemotes[j] = 0x200000 + j;
rec.lastRollingCode = 65000;
rec.flags = 0x21;
rec.myPos = 42.5f;
rec.myTiltPos = 12.25f;
rec.currentPos = 66.0f;
rec.currentTiltPos = 33.5f;
rec.flipCommands = true;
rec.flipPosition = true;
rec.repeats = 4;
rec.sortOrder = 3;
rec.gpioUp = 25;
rec.gpioDown = 26;
rec.gpioMy = 27;
rec.gpioFlags = 2;
rec.roomId = 5;
return rec;
}
static group_record_t aGroup() {
group_record_t rec;
rec.groupId = 3;
rec.remoteAddress = 0x654321;
strcpy(rec.name, "Upstairs");
rec.proto = radio_proto::RTS;
rec.bitLength = 80;
rec.linkedShades[0] = 1;
rec.linkedShades[1] = 7;
rec.linkedShades[2] = 32;
rec.repeats = 2;
rec.sortOrder = 9;
rec.flipCommands = true;
rec.roomId = 4;
rec.lastRollingCode = 1234;
return rec;
}
static net_record_t aNet() {
net_record_t rec;
rec.connType = 2;
rec.dhcp = false;
strcpy(rec.ip, "192.168.100.200");
strcpy(rec.gateway, "192.168.100.1");
strcpy(rec.subnet, "255.255.255.0");
strcpy(rec.dns1, "1.1.1.1");
strcpy(rec.dns2, "8.8.8.8");
strcpy(rec.mqttProtocol, "mqtts://");
strcpy(rec.mqttHostname, "broker.local");
rec.mqttPort = 8883;
rec.mqttPubDisco = true;
strcpy(rec.mqttRootTopic, "somfy");
strcpy(rec.mqttDiscoTopic, "ha");
rec.ethBoardType = 3;
rec.ethPhyType = 1;
rec.ethClkMode = 3;
rec.ethPhyAddress = -1;
rec.ethPWRPin = 12;
rec.ethMDCPin = 23;
rec.ethMDIOPin = 18;
return rec;
}
static trans_record_t aTrans() {
trans_record_t rec;
rec.enabled = true;
rec.type = 80;
rec.SCKPin = 4;
rec.CSNPin = 7;
rec.MOSIPin = 6;
rec.MISOPin = 5;
rec.TXPin = 10;
rec.RXPin = 3;
rec.frequency = 433.25f;
rec.rxBandwidth = 812.5f;
rec.deviation = 1.58f;
rec.txPower = -30;
rec.noiseDetection = true;
return rec;
}
static void assertShadesEqual(const shade_record_t &want, const shade_record_t &got) {
TEST_ASSERT_EQUAL_UINT8(want.shadeId, got.shadeId);
TEST_ASSERT_EQUAL(want.paired, got.paired);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.shadeType), static_cast<uint8_t>(got.shadeType));
TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress);
TEST_ASSERT_EQUAL_STRING(want.name, got.name);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.tiltType), static_cast<uint8_t>(got.tiltType));
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.proto), static_cast<uint8_t>(got.proto));
TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength);
TEST_ASSERT_EQUAL_UINT32(want.upTime, got.upTime);
TEST_ASSERT_EQUAL_UINT32(want.downTime, got.downTime);
TEST_ASSERT_EQUAL_UINT32(want.tiltTime, got.tiltTime);
TEST_ASSERT_EQUAL_UINT16(want.stepSize, got.stepSize);
TEST_ASSERT_EQUAL_HEX32_ARRAY(want.linkedRemotes, got.linkedRemotes, SOMFY_MAX_LINKED_REMOTES);
TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode);
TEST_ASSERT_EQUAL_HEX8(want.flags, got.flags);
TEST_ASSERT_EQUAL_FLOAT(want.myPos, got.myPos);
TEST_ASSERT_EQUAL_FLOAT(want.myTiltPos, got.myTiltPos);
TEST_ASSERT_EQUAL_FLOAT(want.currentPos, got.currentPos);
TEST_ASSERT_EQUAL_FLOAT(want.currentTiltPos, got.currentTiltPos);
TEST_ASSERT_EQUAL(want.flipCommands, got.flipCommands);
TEST_ASSERT_EQUAL(want.flipPosition, got.flipPosition);
TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats);
TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder);
TEST_ASSERT_EQUAL_UINT8(want.gpioUp, got.gpioUp);
TEST_ASSERT_EQUAL_UINT8(want.gpioDown, got.gpioDown);
TEST_ASSERT_EQUAL_UINT8(want.gpioMy, got.gpioMy);
TEST_ASSERT_EQUAL_UINT8(want.gpioFlags, got.gpioFlags);
TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId);
}
// Every record is the size the header says, or validate() refuses the file.
static void test_each_record_is_the_size_the_header_declares() {
config_header_t h = header();
h.length = SHADE_HDR_SIZE;
strcpy(h.serverId, "a1b2c3d4");
TextWriter w;
writeConfigHeader(w, h);
TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, w.text.size());
room_record_t room;
room.roomId = 16;
strcpy(room.name, "12345678901234567890");
w.text.clear();
writeRoomRecord(w, room);
TEST_ASSERT_EQUAL(ROOM_REC_SIZE, w.text.size());
w.text.clear();
writeShadeRecord(w, aShade());
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
w.text.clear();
writeGroupRecord(w, aGroup());
TEST_ASSERT_EQUAL(GROUP_REC_SIZE, w.text.size());
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0xFFFFFF, 1};
w.text.clear();
writeRepeaterRecord(w, repeaters);
TEST_ASSERT_EQUAL(REPEATER_REC_SIZE, w.text.size());
w.text.clear();
writeTransRecord(w, aTrans());
TEST_ASSERT_EQUAL(TRANS_REC_SIZE, w.text.size());
}
// backup() measures the records that vary in length with a ConfigCounter.
static void test_a_counter_measures_what_a_writer_writes() {
TextWriter w;
ConfigCounter c;
writeNetRecord(w, aNet(), false);
writeNetRecord(c, aNet(), false);
TEST_ASSERT_EQUAL(w.text.size(), c.bytes);
}
static void test_a_deleted_shade_is_written_cleared() {
shade_record_t rec = aShade();
rec.shadeId = 255;
TextWriter w;
writeShadeRecord(w, rec);
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
TextReader r(w.text);
shade_record_t got;
readShadeRecord(r, header(), got);
TEST_ASSERT_EQUAL_FLOAT(-1, got.myPos);
TEST_ASSERT_EQUAL_FLOAT(0, got.currentPos);
TEST_ASSERT_EQUAL_HEX8(0, got.flags);
}
static void test_the_header_reads_back() {
config_header_t h = header();
h.length = SHADE_HDR_SIZE;
h.roomRecordSize = ROOM_REC_SIZE;
h.roomRecords = 3;
h.shadeRecordSize = SHADE_REC_SIZE;
h.shadeRecords = 32;
h.groupRecordSize = GROUP_REC_SIZE;
h.groupRecords = 16;
h.repeaterRecordSize = REPEATER_REC_SIZE;
h.repeaterRecords = 1;
h.settingsRecordSize = 94;
h.netRecordSize = 166;
h.transRecordSize = TRANS_REC_SIZE;
strcpy(h.serverId, "a1b2c3d4");
TextWriter w;
writeConfigHeader(w, h);
TextReader r(w.text);
config_header_t got;
readConfigHeader(r, got);
TEST_ASSERT_EQUAL_UINT8(24, got.version);
TEST_ASSERT_EQUAL_INT8(SHADE_HDR_SIZE, got.length);
TEST_ASSERT_EQUAL_UINT8(3, got.roomRecords);
TEST_ASSERT_EQUAL_UINT8(32, got.shadeRecords);
TEST_ASSERT_EQUAL_UINT8(16, got.groupRecords);
TEST_ASSERT_EQUAL_UINT8(REPEATER_REC_SIZE, got.repeaterRecordSize);
TEST_ASSERT_EQUAL_UINT16(94, got.settingsRecordSize);
TEST_ASSERT_EQUAL_UINT16(166, got.netRecordSize);
TEST_ASSERT_EQUAL_UINT16(TRANS_REC_SIZE, got.transRecordSize);
TEST_ASSERT_EQUAL_STRING("a1b2c3d4", got.serverId);
TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, r.position());
}
static void test_a_version_12_header_has_no_rooms_repeaters_or_settings() {
// version, length, shade size and count (one byte each then), group size and count.
TextReader r("12, 20,200, 2,150, 1\n");
config_header_t got;
readConfigHeader(r, got);
TEST_ASSERT_EQUAL_UINT8(12, got.version);
TEST_ASSERT_EQUAL_UINT16(200, got.shadeRecordSize);
TEST_ASSERT_EQUAL_UINT8(2, got.shadeRecords);
TEST_ASSERT_EQUAL_UINT16(150, got.groupRecordSize);
TEST_ASSERT_EQUAL_UINT8(1, got.groupRecords);
TEST_ASSERT_EQUAL_UINT8(0, got.roomRecords);
TEST_ASSERT_EQUAL_UINT8(0, got.repeaterRecords);
TEST_ASSERT_EQUAL_UINT16(0, got.settingsRecordSize);
}
static void test_a_shade_reads_back() {
TextWriter w;
writeShadeRecord(w, aShade());
TextReader r(w.text);
shade_record_t got;
readShadeRecord(r, header(), got);
assertShadesEqual(aShade(), got);
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
static void test_a_room_reads_back() {
room_record_t room;
room.roomId = 2;
strcpy(room.name, "Living room");
room.sortOrder = 5;
TextWriter w;
writeRoomRecord(w, room);
TextReader r(w.text);
room_record_t got;
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
TEST_ASSERT_EQUAL_STRING("Living room", got.name);
TEST_ASSERT_EQUAL_INT8(5, got.sortOrder);
}
static void test_a_group_reads_back() {
TextWriter w;
writeGroupRecord(w, aGroup());
TextReader r(w.text);
group_record_t got;
readGroupRecord(r, header(), got);
const group_record_t want = aGroup();
TEST_ASSERT_EQUAL_UINT8(want.groupId, got.groupId);
TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress);
TEST_ASSERT_EQUAL_STRING(want.name, got.name);
TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength);
TEST_ASSERT_EQUAL_UINT8_ARRAY(want.linkedShades, got.linkedShades, SOMFY_MAX_GROUPED_SHADES);
TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats);
TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder);
TEST_ASSERT_TRUE(got.flipCommands);
TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId);
TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode);
}
static void test_a_group_reads_its_shades_without_gaps() {
group_record_t rec = aGroup();
memset(rec.linkedShades, 0, sizeof(rec.linkedShades));
rec.linkedShades[2] = 4;
rec.linkedShades[10] = 9;
TextWriter w;
writeGroupRecord(w, rec);
TextReader r(w.text);
group_record_t got;
readGroupRecord(r, header(), got);
TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]);
TEST_ASSERT_EQUAL_UINT8(9, got.linkedShades[1]);
TEST_ASSERT_EQUAL_UINT8(0, got.linkedShades[2]);
}
static void test_the_settings_read_back() {
settings_record_t rec;
strcpy(rec.fwVersion, "v4.0.0");
strcpy(rec.hostname, "somfy");
strcpy(rec.ntpServer, "time.google.com");
strcpy(rec.posixZone, "CET-1CEST,M3.5.0,M10.5.0/3");
rec.ssdpBroadcast = true;
rec.checkForUpdate = false;
TextWriter w;
writeSettingsRecord(w, rec);
TextReader r(w.text);
settings_record_t got;
readSettingsRecord(r, header(), got);
TEST_ASSERT_EQUAL_STRING("v4.0.0", got.fwVersion);
TEST_ASSERT_EQUAL_STRING("somfy", got.hostname);
TEST_ASSERT_EQUAL_STRING("time.google.com", got.ntpServer);
// The commas are inside the quotes.
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", got.posixZone);
TEST_ASSERT_TRUE(got.ssdpBroadcast);
TEST_ASSERT_FALSE(got.checkForUpdate);
}
static void test_the_network_reads_back_with_ethernet() {
TextWriter w;
writeNetRecord(w, aNet(), true);
TextReader r(w.text);
net_record_t got;
readNetRecord(r, header(), got, true);
TEST_ASSERT_EQUAL_UINT8(2, got.connType);
TEST_ASSERT_FALSE(got.dhcp);
TEST_ASSERT_EQUAL_STRING("192.168.100.200", got.ip);
TEST_ASSERT_EQUAL_STRING("192.168.100.1", got.gateway);
TEST_ASSERT_EQUAL_STRING("255.255.255.0", got.subnet);
TEST_ASSERT_EQUAL_STRING("1.1.1.1", got.dns1);
TEST_ASSERT_EQUAL_STRING("8.8.8.8", got.dns2);
TEST_ASSERT_EQUAL_STRING("mqtts://", got.mqttProtocol);
TEST_ASSERT_EQUAL_STRING("broker.local", got.mqttHostname);
TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort);
TEST_ASSERT_TRUE(got.mqttPubDisco);
TEST_ASSERT_EQUAL_STRING("somfy", got.mqttRootTopic);
TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic);
TEST_ASSERT_EQUAL_UINT8(3, got.ethBoardType);
TEST_ASSERT_EQUAL_UINT8(1, got.ethPhyType);
TEST_ASSERT_EQUAL_UINT8(3, got.ethClkMode);
TEST_ASSERT_EQUAL_INT8(-1, got.ethPhyAddress);
TEST_ASSERT_EQUAL_INT8(12, got.ethPWRPin);
TEST_ASSERT_EQUAL_INT8(23, got.ethMDCPin);
TEST_ASSERT_EQUAL_INT8(18, got.ethMDIOPin);
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
static void test_the_radio_reads_back() {
TextWriter w;
writeTransRecord(w, aTrans());
TextReader r(w.text);
trans_record_t got;
readTransRecord(r, got);
TEST_ASSERT_TRUE(got.enabled);
TEST_ASSERT_EQUAL_UINT8(80, got.type);
TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin);
TEST_ASSERT_EQUAL_UINT8(7, got.CSNPin);
TEST_ASSERT_EQUAL_UINT8(6, got.MOSIPin);
TEST_ASSERT_EQUAL_UINT8(5, got.MISOPin);
TEST_ASSERT_EQUAL_UINT8(10, got.TXPin);
TEST_ASSERT_EQUAL_UINT8(3, got.RXPin);
TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency);
TEST_ASSERT_EQUAL_FLOAT(812.5f, got.rxBandwidth);
TEST_ASSERT_EQUAL_FLOAT(1.58f, got.deviation);
TEST_ASSERT_EQUAL_INT8(-30, got.txPower);
TEST_ASSERT_TRUE(got.noiseDetection);
}
// The web UI takes any name, and the record would shift by a value at the
// comma.
static void test_a_name_with_a_comma_keeps_the_record_in_line() {
shade_record_t rec = aShade();
strcpy(rec.name, "Left, by the door");
TextWriter w;
writeShadeRecord(w, rec);
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
TextReader r(w.text);
shade_record_t got;
readShadeRecord(r, header(), got);
assertShadesEqual(rec, got);
}
static void test_the_longest_name_reads_back() {
room_record_t room;
room.roomId = 1;
strcpy(room.name, "12345678901234567890");
room.sortOrder = 7;
TextWriter w;
writeRoomRecord(w, room);
TextReader r(w.text);
room_record_t got;
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_STRING("12345678901234567890", got.name);
TEST_ASSERT_EQUAL_INT8(7, got.sortOrder);
}
// A quoted value as long as its field, with its quotes and comma, is longer
// than the field. Reading stopped at the field's size and left the rest to
// the next value.
static void test_the_longest_settings_read_back() {
settings_record_t rec;
strcpy(rec.fwVersion, "v4.0.0");
memset(rec.hostname, 'h', sizeof(rec.hostname) - 1);
memset(rec.ntpServer, 'n', sizeof(rec.ntpServer) - 1);
memset(rec.posixZone, 'z', sizeof(rec.posixZone) - 1);
rec.ssdpBroadcast = true;
rec.checkForUpdate = false;
TextWriter w;
writeSettingsRecord(w, rec);
TextReader r(w.text);
settings_record_t got;
readSettingsRecord(r, header(), got);
TEST_ASSERT_EQUAL_STRING(rec.hostname, got.hostname);
TEST_ASSERT_EQUAL_STRING(rec.ntpServer, got.ntpServer);
TEST_ASSERT_EQUAL_STRING(rec.posixZone, got.posixZone);
TEST_ASSERT_TRUE(got.ssdpBroadcast);
TEST_ASSERT_FALSE(got.checkForUpdate);
}
static void test_the_longest_mqtt_settings_read_back() {
net_record_t rec = aNet();
memset(rec.mqttHostname, 'm', sizeof(rec.mqttHostname) - 1);
memset(rec.mqttRootTopic, 'r', sizeof(rec.mqttRootTopic) - 1);
TextWriter w;
writeNetRecord(w, rec, false);
TextReader r(w.text);
net_record_t got;
readNetRecord(r, header(), got, false);
TEST_ASSERT_EQUAL_STRING(rec.mqttHostname, got.mqttHostname);
TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort);
TEST_ASSERT_EQUAL_STRING(rec.mqttRootTopic, got.mqttRootTopic);
TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic);
}
// A value too long for its field is cut, and the next value still reads.
static void test_a_value_too_long_is_cut() {
TextReader r("\"0123456789012345678901234567890123456789\",\"next\"\n");
char ip[CFG_IP_LEN];
char next[CFG_IP_LEN];
r.beginRecord();
r.readVarString(ip, sizeof(ip));
r.readVarString(next, sizeof(next));
TEST_ASSERT_EQUAL_STRING("01234567890123456789012", ip);
TEST_ASSERT_EQUAL_STRING("next", next);
}
// The layout of a restore past the shades: the network record of a board
// without Ethernet, then the radio record.
static std::string backupTail(const net_record_t &net, const trans_record_t &trans) {
TextWriter w;
writeNetRecord(w, net, false);
writeTransRecord(w, trans);
return w.text;
}
// 4.0 boards without Ethernet declared the network record 28 bytes longer
// than they wrote, and a restore that skipped it by its size read the radio
// from the middle of its record.
static void test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio() {
TextReader r(backupTail(aNet(), aTrans()));
skipRecords(r, 1);
trans_record_t got;
readTransRecord(r, got);
TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin);
TEST_ASSERT_EQUAL_UINT8(3, got.RXPin);
TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency);
TEST_ASSERT_TRUE(got.noiseDetection);
}
// The board that wrote a backup reads its Ethernet settings back; a board
// with Ethernet restoring a backup from one without finds none.
static void test_a_network_record_without_ethernet_keeps_the_ethernet_defaults() {
TextReader r(backupTail(aNet(), aTrans()));
net_record_t got;
readNetRecord(r, header(), got, true);
TEST_ASSERT_EQUAL_UINT8(0, got.ethBoardType);
TEST_ASSERT_EQUAL_INT8(1, got.ethPhyAddress);
TEST_ASSERT_EQUAL_INT8(23, got.ethMDIOPin);
trans_record_t trans;
readTransRecord(r, trans);
TEST_ASSERT_EQUAL_UINT8(4, trans.SCKPin);
}
// Before 4.0 the radio record was declared 74 bytes and is 80.
static void test_the_radio_reads_whole_whatever_its_declared_size() {
TextWriter w;
writeTransRecord(w, aTrans());
TEST_ASSERT_NOT_EQUAL(74, w.text.size());
TextReader r(w.text);
trans_record_t got;
readTransRecord(r, got);
TEST_ASSERT_TRUE(got.noiseDetection);
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
// A record from a newer version has values this one doesn't know.
static void test_values_a_newer_version_added_are_skipped() {
TextWriter w;
writeRoomRecord(w, room_record_t{1, "Hall", 0});
w.text.insert(w.text.size() - 1, ", 9, 9");
writeRoomRecord(w, room_record_t{2, "Attic", 1});
TextReader r(w.text);
room_record_t got;
readRoomRecord(r, got);
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
TEST_ASSERT_EQUAL_STRING("Attic", got.name);
}
// A record shorter than its version says reads defaults for the rest and
// leaves the next record alone.
static void test_a_short_record_never_reads_the_next() {
TextWriter w;
writeRoomRecord(w, room_record_t{1, "Hall", 0});
// Ends after the name.
std::string text = w.text.substr(0, 4 + CFG_NAME_LEN - 1) + CFG_REC_END;
w.text.clear();
writeRoomRecord(w, room_record_t{2, "Attic", 1});
TextReader r(text + w.text);
room_record_t got;
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_UINT8(1, got.roomId);
TEST_ASSERT_EQUAL_STRING("Hall", got.name);
TEST_ASSERT_EQUAL_INT8(0, got.sortOrder);
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
}
static void test_skipping_records_passes_their_newlines() {
TextWriter w;
writeShadeRecord(w, aShade());
writeGroupRecord(w, aGroup());
writeRoomRecord(w, room_record_t{4, "Last", 2});
TextReader r(w.text);
TEST_ASSERT_TRUE(skipRecords(r, 2));
room_record_t got;
readRoomRecord(r, got);
TEST_ASSERT_EQUAL_STRING("Last", got.name);
TEST_ASSERT_FALSE(skipRecords(r, 1));
}
// Older versions, written field by field in the layout they had.
static void writeShadeStart(TextWriter &w, uint8_t id) {
w.writeUInt8(id);
w.writeBool(true);
w.writeUInt8(static_cast<uint8_t>(shade_types::roller));
w.writeUInt32(0x111111);
w.writeString("Old", CFG_NAME_LEN);
}
static std::string version4Shade() {
TextWriter w;
writeShadeStart(w, 2);
w.writeUInt8(0); // tiltType
w.writeUInt8(56); // bitLength
w.writeUInt32(15000); // upTime
w.writeUInt32(14000); // downTime
w.writeUInt32(0); // tiltTime
for(uint8_t j = 0; j < 5; j++) w.writeUInt32(j == 0 ? 0x222222 : 0);
w.writeUInt16(300); // lastRollingCode
w.writeFloat(50, 5); // myPos
w.writeFloat(-1, 5); // myTiltPos
w.writeFloat(25, 5); // currentPos
w.writeFloat(0, 5, CFG_REC_END);
return w.text;
}
static void test_a_version_4_shade() {
const std::string text = version4Shade();
TextReader r(text);
shade_record_t got;
readShadeRecord(r, header(4), got);
TEST_ASSERT_EQUAL_UINT8(2, got.shadeId);
TEST_ASSERT_TRUE(got.paired);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(shade_types::roller), static_cast<uint8_t>(got.shadeType));
TEST_ASSERT_EQUAL_HEX32(0x111111, got.remoteAddress);
TEST_ASSERT_EQUAL_STRING("Old", got.name);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(tilt_types::none), static_cast<uint8_t>(got.tiltType));
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(radio_proto::RTS), static_cast<uint8_t>(got.proto));
TEST_ASSERT_EQUAL_UINT8(56, got.bitLength);
TEST_ASSERT_EQUAL_UINT32(15000, got.upTime);
TEST_ASSERT_EQUAL_UINT32(14000, got.downTime);
TEST_ASSERT_EQUAL_UINT32(0, got.tiltTime);
TEST_ASSERT_EQUAL_UINT16(100, got.stepSize);
TEST_ASSERT_EQUAL_HEX32(0x222222, got.linkedRemotes[0]);
TEST_ASSERT_EQUAL_HEX32(0, got.linkedRemotes[4]);
TEST_ASSERT_EQUAL_HEX32(0, got.linkedRemotes[5]);
TEST_ASSERT_EQUAL_UINT16(300, got.lastRollingCode);
TEST_ASSERT_EQUAL_HEX8(0, got.flags);
TEST_ASSERT_EQUAL_FLOAT(50, got.myPos);
TEST_ASSERT_EQUAL_FLOAT(-1, got.myTiltPos);
TEST_ASSERT_EQUAL_FLOAT(25, got.currentPos);
TEST_ASSERT_EQUAL_FLOAT(0, got.currentTiltPos);
TEST_ASSERT_FALSE(got.flipCommands);
TEST_ASSERT_FALSE(got.flipPosition);
TEST_ASSERT_EQUAL_UINT8(1, got.repeats);
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
TEST_ASSERT_EQUAL_UINT8(0, got.gpioUp);
TEST_ASSERT_EQUAL_UINT8(0, got.roomId);
TEST_ASSERT_EQUAL(text.size(), r.position());
}
// A restore reads into a record that starts as the shade it replaces, and
// what an older version lacks stays as it was.
static void test_what_an_older_version_lacks_stays_as_it_was() {
const std::string text = version4Shade();
TextReader r(text);
shade_record_t got = aShade();
readShadeRecord(r, header(4), got);
TEST_ASSERT_EQUAL_UINT32(15000, got.upTime);
TEST_ASSERT_EQUAL_HEX32(0x222222, got.linkedRemotes[0]);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(radio_proto::GP_Remote), static_cast<uint8_t>(got.proto));
TEST_ASSERT_EQUAL_UINT16(250, got.stepSize);
TEST_ASSERT_EQUAL_HEX32(0x200005, got.linkedRemotes[5]);
TEST_ASSERT_EQUAL_HEX8(0x21, got.flags);
TEST_ASSERT_TRUE(got.flipCommands);
TEST_ASSERT_TRUE(got.flipPosition);
TEST_ASSERT_EQUAL_UINT8(4, got.repeats);
TEST_ASSERT_EQUAL_UINT8(25, got.gpioUp);
TEST_ASSERT_EQUAL_UINT8(27, got.gpioMy);
TEST_ASSERT_EQUAL_UINT8(2, got.gpioFlags);
TEST_ASSERT_EQUAL_UINT8(5, got.roomId);
}
// Version 2 saved positions as fractions and the favorite as a byte.
static void test_a_version_2_shade_has_fractions() {
TextWriter w;
writeShadeStart(w, 1);
w.writeBool(false); // tilt
w.writeUInt8(56);
w.writeUInt32(10000);
w.writeUInt32(10000);
w.writeUInt32(0);
for(uint8_t j = 0; j < 5; j++) w.writeUInt32(0);
w.writeUInt16(12);
w.writeUInt8(40); // myPos
w.writeFloat(0.5f, 5); // currentPos
w.writeFloat(0, 5, CFG_REC_END);
TextReader r(w.text);
shade_record_t got;
readShadeRecord(r, header(2), got);
TEST_ASSERT_EQUAL_FLOAT(40, got.myPos);
TEST_ASSERT_EQUAL_FLOAT(50, got.currentPos);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(tilt_types::none), static_cast<uint8_t>(got.tiltType));
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
// Version 13 had the sort order and no GPIOs or room.
static void test_a_version_13_shade() {
TextWriter w;
writeShadeStart(w, 5);
w.writeUInt8(0);
w.writeUInt8(static_cast<uint8_t>(radio_proto::RTS));
w.writeUInt8(80);
w.writeUInt32(10000);
w.writeUInt32(10000);
w.writeUInt32(0);
w.writeUInt16(200);
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) w.writeUInt32(0);
w.writeUInt16(77);
w.writeUInt8(0x01); // flags
w.writeFloat(-1, 5);
w.writeFloat(-1, 5);
w.writeFloat(100, 5);
w.writeFloat(0, 5);
w.writeBool(true); // flipCommands
w.writeBool(false); // flipPosition
w.writeUInt8(3); // repeats
w.writeUInt8(8, CFG_REC_END);
TextReader r(w.text);
shade_record_t got;
readShadeRecord(r, header(13), got);
TEST_ASSERT_EQUAL_UINT8(80, got.bitLength);
TEST_ASSERT_EQUAL_UINT16(200, got.stepSize);
TEST_ASSERT_EQUAL_UINT16(77, got.lastRollingCode);
TEST_ASSERT_EQUAL_HEX8(0x01, got.flags);
TEST_ASSERT_EQUAL_FLOAT(100, got.currentPos);
TEST_ASSERT_TRUE(got.flipCommands);
TEST_ASSERT_EQUAL_UINT8(3, got.repeats);
TEST_ASSERT_EQUAL_INT8(8, got.sortOrder);
TEST_ASSERT_EQUAL_UINT8(0, got.gpioUp);
TEST_ASSERT_EQUAL_UINT8(0, got.roomId);
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
static void writeGroupStart(TextWriter &w) {
w.writeUInt8(2);
w.writeUInt8(0);
w.writeUInt32(0x333333);
w.writeString("Group", CFG_NAME_LEN);
w.writeUInt8(0);
w.writeUInt8(56);
}
// Version 23 put the group's rolling code before its shades.
static void test_a_version_23_group() {
TextWriter w;
writeGroupStart(w);
w.writeUInt16(4321);
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j < 2 ? j + 1 : 0);
w.writeUInt8(1);
w.writeUInt8(1);
w.writeBool(false);
w.writeUInt8(6, CFG_REC_END);
TextReader r(w.text);
group_record_t got;
readGroupRecord(r, header(23), got);
TEST_ASSERT_EQUAL_UINT16(4321, got.lastRollingCode);
TEST_ASSERT_TRUE(got.hasRollingCode);
TEST_ASSERT_EQUAL_UINT8(2, got.linkedShades[1]);
TEST_ASSERT_EQUAL_UINT8(6, got.roomId);
TEST_ASSERT_EQUAL(w.text.size(), r.position());
}
static void test_a_version_11_group() {
TextWriter w;
writeGroupStart(w);
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j == 0 ? 4 : 0, j == SOMFY_MAX_GROUPED_SHADES - 1 ? CFG_REC_END : CFG_VALUE_SEP);
TextReader r(w.text);
group_record_t got;
readGroupRecord(r, header(11), got);
TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]);
TEST_ASSERT_EQUAL_UINT8(1, got.repeats);
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
TEST_ASSERT_FALSE(got.flipCommands);
TEST_ASSERT_EQUAL_UINT16(0, got.lastRollingCode);
}
// Before version 23 a group had no rolling code, so a restore leaves the one
// NVS holds.
static void test_a_group_before_version_23_has_no_rolling_code() {
TextWriter w;
writeGroupStart(w);
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(0, j == SOMFY_MAX_GROUPED_SHADES - 1 ? CFG_REC_END : CFG_VALUE_SEP);
TextReader r(w.text);
group_record_t got = aGroup();
readGroupRecord(r, header(11), got);
TEST_ASSERT_FALSE(got.hasRollingCode);
TEST_ASSERT_EQUAL_UINT16(1234, got.lastRollingCode);
TEST_ASSERT_TRUE(got.flipCommands);
TEST_ASSERT_EQUAL_UINT8(4, got.roomId);
TextWriter v24;
writeGroupRecord(v24, aGroup());
TextReader r24(v24.text);
readGroupRecord(r24, header(), got);
TEST_ASSERT_TRUE(got.hasRollingCode);
}
// Before version 20 the settings had no update check, which stays as it was.
static void test_version_19_settings_leave_the_update_check() {
TextReader r("\"v2.0.0\",\"host\",\"pool.ntp.org\",\"\",true \n");
settings_record_t got;
got.checkForUpdate = false;
readSettingsRecord(r, header(19), got);
TEST_ASSERT_EQUAL_STRING("host", got.hostname);
TEST_ASSERT_TRUE(got.ssdpBroadcast);
TEST_ASSERT_FALSE(got.checkForUpdate);
}
// Before version 22 the network record had no MQTT settings.
static void test_a_version_21_network_has_no_mqtt() {
TextReader r(" 1,true ,\"\",\"\",\"\",\"\",\"\", 1, 0, 0, 0, -1, 23, 18\n 9\n");
net_record_t got;
readNetRecord(r, header(21), got, true);
TEST_ASSERT_EQUAL_UINT8(1, got.connType);
TEST_ASSERT_EQUAL_STRING("", got.mqttHostname);
TEST_ASSERT_EQUAL_UINT16(1883, got.mqttPort);
TEST_ASSERT_EQUAL_INT8(-1, got.ethPWRPin);
r.beginRecord();
TEST_ASSERT_EQUAL_UINT8(9, r.readUInt8(0));
}
// A real backup, which isn't in the repo: it holds the remotes' addresses.
// CONFIG_BACKUP=<path> pio test -e native -f test_configrecords
static void test_a_real_backup_reads_to_its_end() {
const char *path = getenv("CONFIG_BACKUP");
if(!path) TEST_IGNORE_MESSAGE("set CONFIG_BACKUP to a backup file");
std::ifstream in(path, std::ios::binary);
TEST_ASSERT_TRUE_MESSAGE(in.good(), "can't open CONFIG_BACKUP");
std::stringstream ss;
ss << in.rdbuf();
TextReader r(ss.str());
config_header_t h;
readConfigHeader(r, h);
TEST_ASSERT_EQUAL(static_cast<uint32_t>(h.length), r.position());
for(uint8_t i = 0; i < h.roomRecords; i++) {
room_record_t rec;
readRoomRecord(r, rec);
TEST_ASSERT_NOT_EQUAL(0, rec.roomId);
}
for(uint8_t i = 0; i < h.shadeRecords; i++) {
shade_record_t rec;
readShadeRecord(r, h, rec);
TEST_ASSERT_TRUE(rec.shadeId >= 1 && rec.shadeId <= SOMFY_MAX_SHADES);
TEST_ASSERT_TRUE(rec.currentPos >= 0 && rec.currentPos <= 100);
// Written back, it is the record it was read from.
TextWriter w;
writeShadeRecord(w, rec);
TEST_ASSERT_EQUAL(h.shadeRecordSize, w.text.size());
}
for(uint8_t i = 0; i < h.groupRecords; i++) {
group_record_t rec;
readGroupRecord(r, h, rec);
TEST_ASSERT_TRUE(rec.groupId >= 1 && rec.groupId <= SOMFY_MAX_GROUPS);
}
for(uint8_t i = 0; i < h.repeaterRecords; i++) {
uint32_t repeaters[SOMFY_MAX_REPEATERS];
readRepeaterRecord(r, repeaters);
}
settings_record_t settings;
readSettingsRecord(r, h, settings);
TEST_ASSERT_EQUAL_UINT8('v', settings.fwVersion[0]);
net_record_t net;
readNetRecord(r, h, net, true);
trans_record_t trans;
readTransRecord(r, trans);
TEST_ASSERT_TRUE(trans.frequency > 300 && trans.frequency < 950);
TEST_ASSERT_EQUAL(ss.str().size(), r.position());
}
int main(int argc, char **argv) {
UNITY_BEGIN();
RUN_TEST(test_each_record_is_the_size_the_header_declares);
RUN_TEST(test_a_counter_measures_what_a_writer_writes);
RUN_TEST(test_a_deleted_shade_is_written_cleared);
RUN_TEST(test_the_header_reads_back);
RUN_TEST(test_a_version_12_header_has_no_rooms_repeaters_or_settings);
RUN_TEST(test_a_shade_reads_back);
RUN_TEST(test_a_room_reads_back);
RUN_TEST(test_a_group_reads_back);
RUN_TEST(test_a_group_reads_its_shades_without_gaps);
RUN_TEST(test_the_settings_read_back);
RUN_TEST(test_the_network_reads_back_with_ethernet);
RUN_TEST(test_the_radio_reads_back);
RUN_TEST(test_a_name_with_a_comma_keeps_the_record_in_line);
RUN_TEST(test_the_longest_name_reads_back);
RUN_TEST(test_the_longest_settings_read_back);
RUN_TEST(test_the_longest_mqtt_settings_read_back);
RUN_TEST(test_a_value_too_long_is_cut);
RUN_TEST(test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio);
RUN_TEST(test_a_network_record_without_ethernet_keeps_the_ethernet_defaults);
RUN_TEST(test_the_radio_reads_whole_whatever_its_declared_size);
RUN_TEST(test_values_a_newer_version_added_are_skipped);
RUN_TEST(test_a_short_record_never_reads_the_next);
RUN_TEST(test_skipping_records_passes_their_newlines);
RUN_TEST(test_a_version_4_shade);
RUN_TEST(test_what_an_older_version_lacks_stays_as_it_was);
RUN_TEST(test_a_version_2_shade_has_fractions);
RUN_TEST(test_a_version_13_shade);
RUN_TEST(test_a_version_23_group);
RUN_TEST(test_a_version_11_group);
RUN_TEST(test_a_group_before_version_23_has_no_rolling_code);
RUN_TEST(test_version_19_settings_leave_the_update_check);
RUN_TEST(test_a_version_21_network_has_no_mqtt);
RUN_TEST(test_a_real_backup_reads_to_its_end);
return UNITY_END();
}