mirror of https://github.com/meshcore-dev/MeshCore
committed by
GitHub
15 changed files with 993 additions and 199 deletions
@ -0,0 +1,331 @@ |
|||
#include "ConfigSerializer.h" |
|||
|
|||
bool ConfigSerializer::saveSerial(Stream& s) { |
|||
Context context(&s, OP::WRITE); |
|||
_context = &context; // set the context for structure() call
|
|||
s.print("{"); // root object
|
|||
_first = true; |
|||
structure(); |
|||
if (s.print("}") != 1) context.success = false; // failure detect
|
|||
_context = NULL; |
|||
return context.success; |
|||
} |
|||
|
|||
#define TOK_ERROR -1 |
|||
#define TOK_EOF 0 |
|||
#define TOK_KEY 1 |
|||
#define TOK_VALUE 2 |
|||
#define TOK_START_OBJ 3 |
|||
#define TOK_END_OBJ 4 |
|||
#define TOK_WHITESPACE 5 |
|||
|
|||
static bool is_whitespace(char c) { |
|||
return c == ' ' || c == '\t' || c == '\r' || c == '\n'; |
|||
} |
|||
static bool is_key_char(char c) { |
|||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; |
|||
} |
|||
static bool is_value_char(char c) { |
|||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; |
|||
} |
|||
|
|||
#define EXPECT_OPEN_BRACE 0 |
|||
#define EXPECT_KEY 1 |
|||
#define EXPECT_VAL_OR_OBJ 2 |
|||
#define EXPECT_STRING_VAL 3 |
|||
#define EXPECT_STRING_ESCAPE 4 |
|||
#define EXPECT_COMMA_OR_CLOSE 5 |
|||
#define EXPECT_COMMA_OR_KEY 6 |
|||
#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7 |
|||
|
|||
int ConfigSerializer::Context::readNext() { |
|||
char c; |
|||
if (pending) { |
|||
c = pending; |
|||
pending = 0; |
|||
} else { |
|||
if (_f->available() == 0) return TOK_EOF; |
|||
|
|||
int n = _f->read(); |
|||
if (n < 0) return TOK_EOF; |
|||
c = (char)n; |
|||
} |
|||
|
|||
switch (rd_mode) { |
|||
case EXPECT_OPEN_BRACE: |
|||
if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } |
|||
if (is_whitespace(c)) return TOK_WHITESPACE; |
|||
return TOK_ERROR; |
|||
|
|||
case EXPECT_COMMA_OR_KEY_OR_CLOSE: |
|||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } |
|||
case EXPECT_COMMA_OR_KEY: |
|||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } |
|||
case EXPECT_KEY: |
|||
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } |
|||
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; |
|||
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } |
|||
return TOK_ERROR; |
|||
|
|||
case EXPECT_VAL_OR_OBJ: |
|||
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; |
|||
if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } |
|||
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } |
|||
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } |
|||
if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } |
|||
return TOK_ERROR; |
|||
|
|||
case EXPECT_STRING_ESCAPE: |
|||
if ((c == 'n') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\n'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } |
|||
if ((c == 'r') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\r'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } |
|||
if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } |
|||
return TOK_ERROR; // unsupport escape
|
|||
|
|||
case EXPECT_STRING_VAL: |
|||
if (c == '"') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } |
|||
if (c == '\\') { rd_mode = EXPECT_STRING_ESCAPE; return TOK_WHITESPACE; } |
|||
if (rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } |
|||
return TOK_ERROR; |
|||
|
|||
case EXPECT_COMMA_OR_CLOSE: |
|||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } |
|||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } |
|||
if (is_whitespace(c)) return TOK_WHITESPACE; |
|||
return TOK_ERROR; |
|||
} |
|||
return TOK_ERROR; // unknown mode
|
|||
} |
|||
|
|||
bool ConfigSerializer::loadSerial(Stream& s) { |
|||
Context context(&s, OP::READ); |
|||
_context = &context; // set the context for structure() call
|
|||
uint8_t sp = 0; // object nesting stack pointer
|
|||
int next_tok; |
|||
|
|||
// parse the Json file
|
|||
while ((next_tok = context.readNext()) > TOK_EOF) { |
|||
if (next_tok == TOK_KEY) { |
|||
context.setKey(sp, context.getToken()); |
|||
} else if (next_tok == TOK_VALUE) { |
|||
_depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth)
|
|||
structure(); |
|||
} else if (next_tok == TOK_START_OBJ) { |
|||
if (sp < CONFIG_MAX_DEPTH - 1) { |
|||
sp++; |
|||
} else { |
|||
//Serial.printf("Error: max nesting reached"); // TODO: debug logging
|
|||
context.success = false; |
|||
break; |
|||
} |
|||
} else if (next_tok == TOK_END_OBJ) { |
|||
if (sp > 0) { |
|||
sp--; |
|||
} else { |
|||
//Serial.printf("Error: too many closing '}'"); // TODO: debug logging
|
|||
context.success = false; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
if (sp != 0 || next_tok == TOK_ERROR) { |
|||
context.success = false; // unmatched { }, or other parse error
|
|||
} |
|||
_context = NULL; |
|||
return context.success; |
|||
} |
|||
|
|||
void ConfigSerializer::writeComma() { |
|||
if (_first) { |
|||
_first = false; |
|||
} else { |
|||
_context->file()->print(","); // comma separated properties
|
|||
} |
|||
} |
|||
|
|||
#include <Utils.h> |
|||
|
|||
void ConfigSerializer::def(const char* key, void* value, size_t len) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":\""); |
|||
mesh::Utils::printHex(*_context->file(), (uint8_t*) value, len); |
|||
_context->file()->print("\""); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
memset(value, 0, len); |
|||
mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, char* value, size_t max_len) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":\""); |
|||
char c; |
|||
while ((c = *value++) != 0) { // TODO: handle UTF-8 encoding
|
|||
if (c == '"') { |
|||
_context->file()->print("\\\""); |
|||
} else if (c == '\\') { |
|||
_context->file()->print("\\\\"); |
|||
} else if (c == '\n') { |
|||
_context->file()->print("\\n"); |
|||
} else if (c == '\r') { |
|||
_context->file()->print("\\r"); |
|||
} else { |
|||
_context->file()->print(c); |
|||
} |
|||
} |
|||
_context->file()->print("\""); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
strncpy(value, _context->getToken(), max_len - 1); |
|||
value[max_len - 1] = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, int32_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print(value); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atol(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, uint32_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print(value); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atol(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, int16_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print((int32_t) value, 10); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atol(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, uint16_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print((uint32_t) value, 10); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atoi(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, uint8_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print((uint32_t) value, 10); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atoi(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, int8_t& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print((int32_t) value, 10); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atoi(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, bool& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
_context->file()->print(value ? "true" : "false"); |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number
|
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, double& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
if (value == 0.0) { |
|||
_context->file()->print("0"); // shorter encoding
|
|||
} else { |
|||
_context->file()->print(value, 6); // REVISIT: how many dec places?
|
|||
} |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = atof(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, float& value) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":"); |
|||
if (value == 0.0f) { |
|||
_context->file()->print("0"); // shorter encoding
|
|||
} else { |
|||
_context->file()->print(value, 4); // REVISIT: how many dec places?
|
|||
} |
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
value = (float) atof(_context->getToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) { |
|||
if (_context->op() == OP::WRITE) { |
|||
writeComma(); |
|||
_context->file()->print(key); |
|||
_context->file()->print(":{"); |
|||
sub_obj._context = _context; // inherit the Context
|
|||
sub_obj._first = true; |
|||
sub_obj.structure(); // recurse into sub object
|
|||
if (_context->file()->print("}") != 1) _context->success = false; // failure detect
|
|||
} else { |
|||
if (_context->keyMatch(_depth, key)) { |
|||
sub_obj._context = _context; // inherit the Context
|
|||
sub_obj._depth = _depth + 1; |
|||
sub_obj.structure(); // recurse into sub object
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
#pragma once |
|||
|
|||
#include <Arduino.h> |
|||
|
|||
#ifndef CONFIG_MAX_DEPTH |
|||
#define CONFIG_MAX_DEPTH 8 |
|||
#endif |
|||
|
|||
#ifndef CONFIG_MAX_KEYLEN |
|||
#define CONFIG_MAX_KEYLEN 16 |
|||
#endif |
|||
|
|||
#ifndef CONFIG_MAX_TOKEN_LEN |
|||
#define CONFIG_MAX_TOKEN_LEN 128 |
|||
#endif |
|||
|
|||
class ConfigSerializer { |
|||
bool _first; |
|||
int8_t _depth; |
|||
|
|||
enum OP { READ, WRITE }; |
|||
|
|||
class Context { |
|||
Stream* _f; |
|||
OP _op; |
|||
uint8_t rd_len; |
|||
uint8_t rd_mode; |
|||
char pending; |
|||
char rd_buf[CONFIG_MAX_TOKEN_LEN]; |
|||
char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN]; |
|||
|
|||
public: |
|||
bool success = true; |
|||
Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; } |
|||
OP op() const { return _op; } |
|||
Stream* file() const { return _f; } |
|||
int readNext(); |
|||
const char* getToken() const { return rd_buf; } |
|||
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } |
|||
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } |
|||
}; |
|||
|
|||
Context* _context = NULL; |
|||
|
|||
void writeComma(); |
|||
|
|||
protected: |
|||
ConfigSerializer() { } |
|||
|
|||
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
|
|||
void def(const char* key, void* value, size_t len); // binary blob (encoded in hex)
|
|||
void def(const char* key, int32_t& value); |
|||
void def(const char* key, int16_t& value); |
|||
void def(const char* key, int8_t& value); |
|||
void def(const char* key, uint32_t& value); |
|||
void def(const char* key, uint16_t& value); |
|||
void def(const char* key, uint8_t& value); |
|||
void def(const char* key, float& value); |
|||
void def(const char* key, double& value); |
|||
void def(const char* key, bool& value); |
|||
void def(const char* key, ConfigSerializer& sub_obj); |
|||
|
|||
virtual void structure() = 0; |
|||
|
|||
public: |
|||
bool loadSerial(Stream& s); |
|||
bool saveSerial(Stream& s); |
|||
}; |
|||
@ -1,23 +1,73 @@ |
|||
#pragma once |
|||
|
|||
#include <cstddef> |
|||
#include <cstdint> |
|||
#include <stddef.h> |
|||
#include <stdint.h> |
|||
#include <string.h> |
|||
|
|||
class Stream { |
|||
// Mock Stream class for native testing
|
|||
// Provides minimal interface needed by Utils.h
|
|||
|
|||
#define DEC 10 |
|||
#define HEX 16 |
|||
#define OCT 8 |
|||
#define BIN 2 |
|||
|
|||
class Print |
|||
{ |
|||
public: |
|||
virtual ~Stream() = default; |
|||
virtual size_t write(uint8_t) { return 1; } |
|||
virtual size_t write(const uint8_t* buffer, size_t size) { |
|||
size_t total = 0; |
|||
for (size_t i = 0; i < size; i++) { |
|||
total += write(buffer[i]); |
|||
virtual size_t write(uint8_t b) { return 1; } |
|||
size_t write(const char *str) |
|||
{ |
|||
if(str == NULL) { |
|||
return 0; |
|||
} |
|||
return total; |
|||
return write((const uint8_t *) str, strlen(str)); |
|||
} |
|||
virtual size_t write(const uint8_t *buffer, size_t size) { |
|||
size_t t = 0; |
|||
for (int i = 0; i < size; i++) { t += write(buffer[i]); } |
|||
return t; |
|||
} |
|||
size_t write(const char *buffer, size_t size) |
|||
{ |
|||
return write((const uint8_t *) buffer, size); |
|||
} |
|||
|
|||
virtual size_t print(unsigned char b, int r = DEC) { return 0; } |
|||
virtual size_t print(int v, int r = DEC) { return 0; } |
|||
virtual size_t print(unsigned int v, int r = DEC) { return 0; } |
|||
virtual size_t print(long v, int r = DEC) { return 0; } |
|||
virtual size_t print(unsigned long v, int r = DEC) { return 0; } |
|||
virtual size_t print(long long v, int r = DEC) { return 0; } |
|||
virtual size_t print(unsigned long long v, int r = DEC) { return 0; } |
|||
virtual size_t print(double v, int p = 2) { return 0; } |
|||
|
|||
size_t print(char c) { return write(c); } |
|||
size_t print(const char* str) { return write(str); } |
|||
|
|||
//size_t println(void) { return 0; }
|
|||
|
|||
virtual void flush() { /* Empty implementation for backward compatibility */ } |
|||
}; |
|||
|
|||
class Stream: public Print |
|||
{ |
|||
public: |
|||
virtual ~Stream() = default; |
|||
virtual int available() { return 0; } |
|||
virtual int availableForWrite() { return 0; } |
|||
virtual int read() { return -1; } |
|||
virtual void flush() {} |
|||
virtual void print(char) {} |
|||
virtual void print(const char*) {} |
|||
}; |
|||
virtual int peek() { return 0; } |
|||
|
|||
virtual size_t readBytes(char *buffer, size_t length) { |
|||
size_t i = 0; |
|||
while (i < length && available()) { |
|||
buffer[i++] = read(); |
|||
} |
|||
return i; |
|||
} |
|||
virtual size_t readBytes(uint8_t *buffer, size_t length) |
|||
{ |
|||
return readBytes((char *) buffer, length); |
|||
} |
|||
}; |
|||
@ -0,0 +1,180 @@ |
|||
#include <gtest/gtest.h> |
|||
#include "helpers/ConfigSerializer.h" |
|||
|
|||
#define TEST_INT_S "56" |
|||
#define TEST_INT 56 |
|||
#define TEST_FLOAT_S "-6.123" |
|||
#define TEST_FLOAT -6.1230f |
|||
#define TEST_DOUBLE_S "12.123456" |
|||
#define TEST_DOUBLE 12.123456 |
|||
|
|||
class MockInputStream : public Stream { |
|||
const char* _text; |
|||
int pos, len; |
|||
public: |
|||
MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); } |
|||
int available() override { return len - pos; } |
|||
int read() override { if (pos < len) { return _text[pos++]; } return -1; } |
|||
int peek() override { if (pos < len) { return _text[pos]; } return -1; } |
|||
}; |
|||
|
|||
class MockPrintStream : public Stream { |
|||
int len = 0; |
|||
uint8_t _buf[1024]; |
|||
public: |
|||
size_t write(uint8_t b) override { |
|||
if (len < sizeof(_buf)) { |
|||
_buf[len++] = b; |
|||
return 1; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } |
|||
size_t print(double v, int p = 2) override { |
|||
if (p == 6) return Print::print(TEST_DOUBLE_S); |
|||
if (p == 4) return Print::print(TEST_FLOAT_S); |
|||
return 0; |
|||
} |
|||
|
|||
int getLength() const { return len; } |
|||
const uint8_t* getBytes() const { return _buf; } |
|||
}; |
|||
|
|||
class TestStruct : public ConfigSerializer { |
|||
protected: |
|||
void structure() override { |
|||
def("age", age); |
|||
def("flags", flags); |
|||
def("name", name, sizeof(name)); |
|||
} |
|||
public: |
|||
int32_t age; |
|||
char name[16]; |
|||
uint8_t flags; |
|||
}; |
|||
|
|||
// ── saveSerial: basic ───────────────────────────────────────────────────────
|
|||
|
|||
TEST(ConfigSerializer, SaveSerial_Basic) { |
|||
MockPrintStream s; |
|||
TestStruct data; |
|||
|
|||
data.age = TEST_INT; |
|||
data.flags = TEST_INT; |
|||
strcpy(data.name, "Scott"); |
|||
|
|||
bool success = data.saveSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
auto l = s.getLength(); |
|||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"; |
|||
EXPECT_EQ(strlen(expect), l); |
|||
|
|||
bool match = memcmp(s.getBytes(), expect, l) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
|
|||
TEST(ConfigSerializer, SaveSerial_EscChars) { |
|||
MockPrintStream s; |
|||
TestStruct data; |
|||
|
|||
data.age = TEST_INT; |
|||
data.flags = TEST_INT; |
|||
strcpy(data.name, "\"Scott\"\n"); |
|||
|
|||
bool success = data.saveSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
auto l = s.getLength(); |
|||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"; |
|||
EXPECT_EQ(strlen(expect), l); |
|||
|
|||
bool match = memcmp(s.getBytes(), expect, l) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
// ── loadSerial: basic ───────────────────────────────────────────────────────
|
|||
|
|||
TEST(ConfigSerializer, LoadSerial_Basic) { |
|||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"); |
|||
TestStruct data; |
|||
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
EXPECT_EQ(TEST_INT, data.age); |
|||
EXPECT_EQ(TEST_INT, data.flags); |
|||
bool match = strcmp("Scott", data.name) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
TEST(ConfigSerializer, LoadSerial_HandleWhitespace) { |
|||
MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } "); |
|||
TestStruct data; |
|||
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
EXPECT_EQ(TEST_INT, data.age); |
|||
EXPECT_EQ(TEST_INT, data.flags); |
|||
bool match = strcmp("Scott", data.name) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
TEST(ConfigSerializer, LoadSerial_EscChars) { |
|||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"); |
|||
TestStruct data; |
|||
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
bool match = strcmp("\"Scott\"\n", data.name) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) { |
|||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\""); |
|||
TestStruct data; |
|||
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_FALSE(success); |
|||
} |
|||
|
|||
TEST(ConfigSerializer, LoadSerial_MissingCommas) { |
|||
MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}"); |
|||
TestStruct data; |
|||
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_FALSE(success); |
|||
} |
|||
|
|||
TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { |
|||
MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}"); |
|||
TestStruct data; |
|||
data.flags = 1; |
|||
|
|||
// should ignore the 'xxx' property
|
|||
bool success = data.loadSerial(s); |
|||
EXPECT_TRUE(success); |
|||
|
|||
EXPECT_EQ(TEST_INT, data.age); |
|||
EXPECT_EQ(1, data.flags); // flags should be unmodified
|
|||
bool match = strcmp("Scott", data.name) == 0; |
|||
EXPECT_TRUE(match); |
|||
} |
|||
|
|||
|
|||
// ── main ───────────────────────────────────────────────────────
|
|||
|
|||
int main(int argc, char** argv) { |
|||
::testing::InitGoogleTest(&argc, argv); |
|||
return RUN_ALL_TESTS(); |
|||
} |
|||
Loading…
Reference in new issue