Something went wrong. Try again.
Live STIB-MIVB departures on the Pebble Time 2. It covers the bus, tram, and metro network of Brussels
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879#include <assert.h>#include "msg.h"#include "model.h"#include "line_window.h"
static uint8_t g_masks[16];static int g_updates;static int g_polls;static int g_fails;
static void on_update(uint8_t mask) { g_masks[g_updates < 16 ? g_updates : 15] = mask; g_updates++; }void line_window_poll(void) { g_polls++; }void line_window_fail(void) { g_fails++; }
static uint8_t g_fav_results[4];static int g_fav_result_count;void departures_window_fav_result(uint8_t result) { g_fav_results[g_fav_result_count < 4 ? g_fav_result_count : 3] = result; g_fav_result_count++;}
#define MAX_TUPLES 24#define ROW_BLOB_ENTRY 57#define STRIP_BLOB_ENTRY 33
typedef struct { Tuple tuples[MAX_TUPLES]; uint16_t count;} TestDict;
static TupleValue *alloc_value(size_t size) { if (size < sizeof(TupleValue)) { size = sizeof(TupleValue); } return malloc(size);}
static void td_int(TestDict *d, uint32_t key, int32_t value) { TupleValue *v = alloc_value(sizeof(int32_t)); v->int32 = value; d->tuples[d->count++] = (Tuple){ .key = key, .length = 4, .value = v };}
static void td_str(TestDict *d, uint32_t key, const char *s) { size_t n = strlen(s) + 1; TupleValue *v = alloc_value(n); memcpy(v->cstring, s, n); d->tuples[d->count++] = (Tuple){ .key = key, .length = (uint16_t)n, .value = v };}
static void td_data(TestDict *d, uint32_t key, const uint8_t *bytes, uint16_t len) { TupleValue *v = alloc_value(len ? len : 1); memcpy(v->data, bytes, len); d->tuples[d->count++] = (Tuple){ .key = key, .length = len, .value = v };}
static void td_send(TestDict *d) { DictionaryIterator it = { d->tuples, d->count }; pebble_stub_inbox(&it, NULL); for (uint16_t i = 0; i < d->count; i++) { free(d->tuples[i].value); } d->count = 0;}
static char g_long[512];
static void blob_str(uint8_t *base, uint16_t off, uint16_t len, const char *s) { strncpy((char *)base + off, s, len - 1);}
static void blob_i16(uint8_t *base, uint16_t off, int16_t v) { base[off] = (uint8_t)(v & 0xFF); base[off + 1] = (uint8_t)((v >> 8) & 0xFF);}
static void blob_row(uint8_t *blob, uint8_t i, const char *line, uint8_t kind, uint8_t flags, const char *dest, int16_t min1, int16_t min2, const char *platform, int16_t delay) { uint8_t *base = blob + 2 + i * ROW_BLOB_ENTRY; memset(base, 0, ROW_BLOB_ENTRY); blob_str(base, 0, 8, line); base[8] = (uint8_t)((kind & 7) | (flags << 3)); base[9] = 0xC0; base[10] = 0xFF; blob_str(base, 11, 32, dest); blob_i16(base, 43, min1); blob_i16(base, 45, min2); blob_str(base, 47, 8, platform); blob_i16(base, 55, delay);}
static void send_stop_v2(uint8_t index, uint8_t count, const char *id, const char *name, const uint8_t *blob, uint16_t blob_len) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_STOP_INDEX, index); td_int(&d, MESSAGE_KEY_STOP_COUNT, count); td_str(&d, MESSAGE_KEY_STOP_ID, id); td_str(&d, MESSAGE_KEY_STOP_NAME, name); td_data(&d, MESSAGE_KEY_BOARD_ROWS, blob, blob_len); td_send(&d);}
static void send_stop(uint8_t index, uint8_t count, const char *id, const char *name) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_STOP_INDEX, index); td_int(&d, MESSAGE_KEY_STOP_COUNT, count); td_str(&d, MESSAGE_KEY_STOP_ID, id); td_str(&d, MESSAGE_KEY_STOP_NAME, name); td_send(&d);}
static void send_stop_counts(uint8_t index, uint8_t count, const char *id, const char *name, int16_t mechanical, int16_t electrical, int16_t docks) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_STOP_INDEX, index); td_int(&d, MESSAGE_KEY_STOP_COUNT, count); td_str(&d, MESSAGE_KEY_STOP_ID, id); td_str(&d, MESSAGE_KEY_STOP_NAME, name); td_int(&d, MESSAGE_KEY_VILLO_MECHANICAL, mechanical); td_int(&d, MESSAGE_KEY_VILLO_ELECTRICAL, electrical); td_int(&d, MESSAGE_KEY_VILLO_DOCKS, docks); td_send(&d);}
static void send_done_ok(void) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 0); td_send(&d);}
static void test_stop_and_villo_fill(void) { send_stop_counts(0, 2, "6419", "BOTANIQUE", 4, 0, 9); send_stop(1, 2, "6001", "OTHER"); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 2); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(strcmp(stop->id, "6419") == 0); assert(strcmp(stop->name, "BOTANIQUE") == 0); assert(stop->bikes_mechanical == 4); assert(stop->bikes_electrical == 0); assert(stop->docks == 9);}
static void test_villo_absent_defaults(void) { send_stop(0, 1, "6419", "BOTANIQUE"); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop->bikes_mechanical == -1); assert(stop->bikes_electrical == -1); assert(stop->docks == -1);}
static void test_board_rows_blob_fill(void) { uint8_t blob[2 + 2 * ROW_BLOB_ENTRY] = { 2, 2 }; blob_row(blob, 0, "62", 0, 1, "SINT-LAMBERTUS", 1, 11, "", 0); blob_row(blob, 1, "IC", 3, 0, "GENT", 5, -1, "4", -60); send_stop_v2(0, 1, "6419", "BOTANIQUE", blob, sizeof(blob)); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop->row_count == 2); assert(strcmp(stop->rows[0].line, "62") == 0); assert(stop->rows[0].flags == 1); assert(stop->rows[0].min1 == 1 && stop->rows[0].min2 == 11); assert(stop->rows[0].color.argb == 0xC0); assert(stop->rows[0].text_color.argb == 0xFF); assert(stop->rows[0].platform[0] == '\0'); assert(stop->rows[0].delay_seconds == 0); assert(strcmp(stop->rows[0].destination, "SINT-LAMBERTUS") == 0); assert(stop->rows[1].kind == 3); assert(strcmp(stop->rows[1].platform, "4") == 0); assert(stop->rows[1].delay_seconds == -60); assert(stop->rows[1].min2 == -1);}
static void test_board_rows_blob_bounds(void) { send_stop(0, 1, "KEEP", "K"); send_done_ok(); assert(model_state() == STATE_OK); uint8_t blob[2 + ROW_BLOB_ENTRY] = { 2, 5 }; blob_row(blob, 0, "7", 0, 0, "X", 1, -1, "", 0); send_stop_v2(0, 1, "S", "N", blob, sizeof(blob)); model_set_state(STATE_LOADING); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop && strcmp(stop->id, "KEEP") == 0); assert(stop->row_count == 0); assert(model_state() == STATE_LOADING);}
static void test_board_rows_wrong_version(void) { send_stop(0, 1, "KEEP", "K"); send_done_ok(); uint8_t blob[2 + ROW_BLOB_ENTRY] = { 1, 1 }; blob_row(blob, 0, "7", 0, 0, "X", 1, -1, "", 0); send_stop_v2(0, 1, "S", "N", blob, sizeof(blob)); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop && strcmp(stop->id, "KEEP") == 0); assert(stop->row_count == 0);}
static void test_board_rows_short_blob(void) { send_stop(0, 1, "KEEP2", "K"); send_done_ok(); uint8_t version_only = 2; send_stop_v2(0, 1, "S", "N", &version_only, 1); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "KEEP2") == 0); send_stop_v2(0, 1, "S", "N", &version_only, 0); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "KEEP2") == 0); uint8_t empty[2] = { 2, 0 }; send_stop_v2(0, 1, "S", "N", empty, sizeof(empty)); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "S") == 0); assert(model_stop_at(PAGE_TRANSIT, 0)->row_count == 0);}
static void test_line_strips_blob(void) { TestDict d = {0}; uint8_t blob[2 + 2 * STRIP_BLOB_ENTRY] = { 2, 2 }; memset(blob + 2, 0, sizeof(blob) - 2); strncpy((char *)blob + 2, "STOP A", 31); blob[2 + 32] = 2; strncpy((char *)blob + 2 + STRIP_BLOB_ENTRY, "STOP B", 31); blob[2 + STRIP_BLOB_ENTRY + 32] = 1; td_int(&d, MESSAGE_KEY_KIND, 6); td_str(&d, MESSAGE_KEY_ROW_LINE, "62"); td_data(&d, MESSAGE_KEY_LINE_STRIPS, blob, sizeof(blob)); td_send(&d); LineDetail *detail = model_line_detail(); assert(detail->strip_count == 2); assert(detail->strip_received == 2); assert(model_line_complete()); assert(strcmp(detail->strip[0].name, "STOP A") == 0); assert(detail->strip[0].marker == 2); assert(detail->strip[1].marker == 1);}
static void send_strips(const uint8_t *blob, uint16_t len) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 6); td_str(&d, MESSAGE_KEY_ROW_LINE, "62"); td_data(&d, MESSAGE_KEY_LINE_STRIPS, blob, len); td_send(&d);}
static void test_line_strips_blob_bounds(void) { uint8_t big[2 + 9 * STRIP_BLOB_ENTRY] = { 2, 9 }; memset(big + 2, 0, sizeof(big) - 2); send_strips(big, sizeof(big)); LineDetail *detail = model_line_detail(); assert(detail->strip_count == 0); assert(detail->strip_received == 0); assert(model_line_complete());
uint8_t full[2 + 8 * STRIP_BLOB_ENTRY] = { 2, 8 }; memset(full + 2, 0, sizeof(full) - 2); strncpy((char *)full + 2, "FULL", 31); send_strips(full, sizeof(full)); assert(detail->strip_count == MAX_STRIP); assert(detail->strip_received == MAX_STRIP); assert(strcmp(detail->strip[0].name, "FULL") == 0); assert(model_line_complete());
uint8_t overshoot[2 + STRIP_BLOB_ENTRY] = { 2, 5 }; memset(overshoot + 2, 0, sizeof(overshoot) - 2); strncpy((char *)overshoot + 2, "ONLY", 31); send_strips(overshoot, sizeof(overshoot)); assert(detail->strip_count == 0);
uint8_t wrong_version[2 + STRIP_BLOB_ENTRY] = { 1, 1 }; memset(wrong_version + 2, 0, sizeof(wrong_version) - 2); send_strips(wrong_version, sizeof(wrong_version)); assert(detail->strip_count == 0); assert(detail->strip_received == 0); assert(model_line_complete());
uint8_t version_only = 2; send_strips(&version_only, 1); assert(detail->strip_count == 0);}
static void test_string_termination(void) { memset(g_long, 'x', sizeof(g_long) - 1); g_long[sizeof(g_long) - 1] = '\0'; uint8_t blob[2 + ROW_BLOB_ENTRY] = { 2, 1 }; memset(blob + 2, 0, ROW_BLOB_ENTRY); memset(blob + 2, 'x', 8); memset(blob + 2 + 11, 'x', 32); memset(blob + 2 + 47, 'x', 8); send_stop_v2(0, 1, g_long, g_long, blob, sizeof(blob)); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(strlen(stop->id) == ID_LEN - 1); assert(strlen(stop->name) == NAME_LEN - 1); assert(strlen(stop->rows[0].line) == LINE_LEN - 1); assert(strlen(stop->rows[0].destination) == NAME_LEN - 1); assert(strlen(stop->rows[0].platform) == PLATFORM_LEN - 1); TestDict d = {0}; uint8_t strips[2 + STRIP_BLOB_ENTRY] = { 2, 1 }; memset(strips + 2, 'x', 32); strips[2 + 32] = 0; td_int(&d, MESSAGE_KEY_KIND, 6); td_str(&d, MESSAGE_KEY_ROW_LINE, g_long); td_str(&d, MESSAGE_KEY_ROW_DEST, g_long); td_str(&d, MESSAGE_KEY_LINE_DISRUPTION, g_long); td_data(&d, MESSAGE_KEY_LINE_STRIPS, strips, sizeof(strips)); td_send(&d); LineDetail *detail = model_line_detail(); assert(strlen(detail->line) == LINE_LEN - 1); assert(strlen(detail->destination) == NAME_LEN - 1); assert(strlen(detail->disruption) == sizeof(detail->disruption) - 1); assert(strlen(detail->strip[0].name) == NAME_LEN - 1);}
static void test_int32_extremes(void) { int32_t extremes[4] = { INT32_MIN, -1, INT32_MAX, 255 }; for (int e = 0; e < 4; e++) { int32_t x = extremes[e]; TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_PAGE, x); td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_int(&d, MESSAGE_KEY_STOP_COUNT, x); td_int(&d, MESSAGE_KEY_STOP_DISTANCE, x); td_send(&d); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) <= MAX_STOPS); assert(model_stop_count(PAGE_TRAINS) <= MAX_STOPS); assert(model_stop_count(PAGE_VILLO) <= MAX_STOPS); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_int(&d, MESSAGE_KEY_STOP_COUNT, 1); td_int(&d, MESSAGE_KEY_STOP_DISTANCE, x); td_int(&d, MESSAGE_KEY_VILLO_MECHANICAL, x); td_int(&d, MESSAGE_KEY_VILLO_ELECTRICAL, x); td_int(&d, MESSAGE_KEY_VILLO_DOCKS, x); td_send(&d); send_done_ok(); Stop *stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop->row_count <= MAX_ROWS); assert(stop->bikes_mechanical == (int16_t)x); assert(stop->bikes_electrical == (int16_t)x); assert(stop->docks == (int16_t)x); d.count = 0; uint8_t blob[2 + ROW_BLOB_ENTRY] = { 2, 1 }; blob_row(blob, 0, "62", 0, 0, "X", (int16_t)x, (int16_t)x, "", (int16_t)x); send_stop_v2(0, 1, "S", "N", blob, sizeof(blob)); send_done_ok(); stop = model_stop_at(PAGE_TRANSIT, 0); assert(stop->rows[0].min1 == (int16_t)x); assert(stop->rows[0].min2 == (int16_t)x); assert(stop->rows[0].delay_seconds == (int16_t)x); td_int(&d, MESSAGE_KEY_KIND, 6); td_int(&d, MESSAGE_KEY_ROW_MIN1, x); td_int(&d, MESSAGE_KEY_ROW_MIN2, x); td_int(&d, MESSAGE_KEY_ROW_KIND, x); td_int(&d, MESSAGE_KEY_LINE_STATUS, x); td_send(&d); LineDetail *detail = model_line_detail(); assert(detail->strip_count <= MAX_STRIP); assert(detail->min1 == (int16_t)x); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, x); td_send(&d); assert(model_state() == STATE_OK || model_state() == STATE_STALE || model_state() == STATE_EMPTY); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 5); td_int(&d, MESSAGE_KEY_CFG_VIBRATE, x); td_int(&d, MESSAGE_KEY_CFG_LANG, x); td_send(&d); assert(model_lang() == (uint8_t)x); }}
static void test_pseudo_prefix_ids_stay_distinct(void) { send_stop(0, 2, "SNCB:8812005", "NOORD"); send_stop(1, 2, "SNCB:8813003", "CENTRAAL"); send_done_ok(); Stop *a = model_stop_by_id(PAGE_TRANSIT, "SNCB:8812005"); Stop *b = model_stop_by_id(PAGE_TRANSIT, "SNCB:8813003"); assert(a != NULL); assert(b != NULL); assert(a != b); assert(strcmp(a->id, "SNCB:8812005") == 0); assert(strcmp(b->id, "SNCB:8813003") == 0);}
static void test_done_message_states(void) { send_stop(0, 1, "6419", "BOTANIQUE"); int polls = g_polls; TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 0); td_send(&d); assert(model_state() == STATE_OK); assert(g_polls == polls + 1); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 3); td_send(&d); assert(model_state() == STATE_STALE); assert(model_stop_count(PAGE_TRANSIT) == 1); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 4); td_send(&d); assert(model_state() == STATE_EMPTY); assert(model_stop_count(PAGE_TRANSIT) == 0);}
static void test_kind3_stamps_arrival_for_staleness(void) { model_set_state(STATE_OK); TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 0); td_send(&d); time_t now = time(NULL); assert(model_check_staleness(PAGE_TRANSIT, now) == STALENESS_NONE); assert(model_check_staleness(PAGE_TRANSIT, now + STALE_THRESHOLD_S + 1) == STALENESS_NUDGE); assert(model_check_staleness(PAGE_TRANSIT, now + STALE_THRESHOLD_S + 2) == STALENESS_NONE);}
static void test_error_message_states(void) { int fails = g_fails; TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 4); td_int(&d, MESSAGE_KEY_ERROR, 1); td_send(&d); assert(model_state() == STATE_GEO_ERROR); assert(g_fails == fails + 1); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 4); td_int(&d, MESSAGE_KEY_ERROR, 2); td_send(&d); assert(model_state() == STATE_NET_ERROR); d.count = 0; model_set_state(STATE_OK); td_int(&d, MESSAGE_KEY_KIND, 4); td_send(&d); assert(model_state() == STATE_OK); assert(g_fails == fails + 3);}
static void test_vibrate_on_zero(void) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 5); td_int(&d, MESSAGE_KEY_CFG_VIBRATE, 1); td_int(&d, MESSAGE_KEY_CFG_LANG, 0); td_send(&d); uint8_t blob[2 + ROW_BLOB_ENTRY] = { 2, 1 }; blob_row(blob, 0, "VZ", 0, 0, "TERM", 1, 5, "", 0); send_stop_v2(0, 1, "VIBSTOP", "V", blob, sizeof(blob)); int vibes = pebble_stub_vibes; send_done_ok(); assert(pebble_stub_vibes == vibes); blob_row(blob, 0, "VZ", 0, 0, "TERM", 1, 5, "", 0); send_stop_v2(0, 1, "VIBSTOP", "V", blob, sizeof(blob)); send_done_ok(); assert(pebble_stub_vibes == vibes); blob_row(blob, 0, "VZ", 0, 0, "TERM", 0, 5, "", 0); send_stop_v2(0, 1, "VIBSTOP", "V", blob, sizeof(blob)); send_done_ok(); assert(pebble_stub_vibes == vibes + 1);}
static void send_shape(const uint8_t *bytes, uint16_t len) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 6); td_data(&d, MESSAGE_KEY_LINE_SHAPE, bytes, len); td_send(&d);}
static void test_shape_byte_caps(void) { uint8_t bytes[256]; for (int i = 0; i < 256; i++) { bytes[i] = (uint8_t)(251 * i + 7); } uint16_t lengths[7] = { 0, 1, 7, 47, 48, 49, 200 }; for (int i = 0; i < 7; i++) { send_shape(bytes, lengths[i]); LineDetail *detail = model_line_detail(); assert(detail->shape_len <= MAX_SHAPE_BYTES); uint16_t expect = lengths[i] > MAX_SHAPE_BYTES ? MAX_SHAPE_BYTES : lengths[i]; assert(detail->shape_len == expect); assert(memcmp(detail->shape, bytes, detail->shape_len) == 0); }}
static void test_ignores_incomplete_dicts(void) { uint8_t before = model_stop_count(PAGE_TRANSIT); TestDict d = {0}; td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_send(&d); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 1); td_str(&d, MESSAGE_KEY_STOP_NAME, "GHOST"); td_send(&d); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 2); td_send(&d); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 7); td_send(&d); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 99); td_send(&d); assert(model_stop_count(PAGE_TRANSIT) == before);}
static void test_page_routes_stops_and_defaults_to_transit(void) { msg_request_refresh(PAGE_TRAINS); TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_PAGE, PAGE_TRAINS); td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_int(&d, MESSAGE_KEY_STOP_COUNT, 1); td_str(&d, MESSAGE_KEY_STOP_ID, "STATION1"); td_send(&d); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_PAGE, PAGE_TRAINS); td_int(&d, MESSAGE_KEY_ERROR, 0); td_send(&d); assert(model_stop_count(PAGE_TRAINS) == 1); assert(strcmp(model_stop_at(PAGE_TRAINS, 0)->id, "STATION1") == 0); assert(model_stop_count(PAGE_TRANSIT) == 0); assert(model_stop_count(PAGE_VILLO) == 0);
msg_request_refresh(PAGE_TRANSIT); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_int(&d, MESSAGE_KEY_STOP_COUNT, 1); td_str(&d, MESSAGE_KEY_STOP_ID, "STOP1"); td_send(&d); assert(model_stop_count(PAGE_TRAINS) == 1); assert(strcmp(model_stop_at(PAGE_TRAINS, 0)->id, "STATION1") == 0); assert(model_stop_count(PAGE_TRANSIT) == 0); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "STOP1") == 0); assert(model_stop_count(PAGE_TRAINS) == 0);
msg_request_refresh(PAGE_VILLO); d.count = 0; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_PAGE, PAGE_VILLO); td_int(&d, MESSAGE_KEY_ERROR, 4); td_send(&d); assert(model_stop_count(PAGE_VILLO) == 0); assert(model_stop_count(PAGE_TRANSIT) == 0); assert(model_stop_count(PAGE_TRAINS) == 0); assert(model_state() == STATE_EMPTY); msg_request_refresh(PAGE_TRANSIT);}
static void send_stop_with_page(int32_t page_value, const char *id) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 1); td_int(&d, MESSAGE_KEY_PAGE, page_value); td_int(&d, MESSAGE_KEY_STOP_INDEX, 0); td_int(&d, MESSAGE_KEY_STOP_COUNT, 1); td_str(&d, MESSAGE_KEY_STOP_ID, id); td_send(&d);}
static void test_out_of_range_page_does_not_corrupt_model(void) { msg_request_refresh(PAGE_TRANSIT); send_stop_with_page(200, "BIG"); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "BIG") == 0);
send_stop_with_page(-1, "NEG"); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "NEG") == 0);
send_stop_with_page(PAGE_COUNT, "EXACT"); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "EXACT") == 0);
assert(model_stop_count(PAGE_TRAINS) == 0); assert(model_stop_count(PAGE_VILLO) == 0);}
static void send_done_with_page(int32_t page, uint8_t code) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_PAGE, page); td_int(&d, MESSAGE_KEY_ERROR, code); td_send(&d);}
static void test_partial_board_never_commits(void) { send_stop(0, 1, "FULLBOARD", "F"); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "FULLBOARD") == 0); send_stop(0, 3, "PART0", "P"); send_stop(2, 3, "PART2", "P"); model_set_state(STATE_LOADING); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "FULLBOARD") == 0); assert(model_state() == STATE_LOADING);}
static void test_late_old_page_reply_cannot_evict_request(void) { send_stop(0, 1, "CURRENT", "C"); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "CURRENT") == 0); msg_request_refresh(PAGE_TRAINS); send_stop(0, 1, "LATE", "L"); model_set_state(STATE_LOADING); send_done_ok(); assert(model_stop_count(PAGE_TRAINS) == 0); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "CURRENT") == 0); assert(model_state() == STATE_LOADING); send_stop_with_page(PAGE_TRAINS, "TR1"); send_done_with_page(PAGE_TRAINS, 0); assert(model_stop_count(PAGE_TRAINS) == 1); assert(strcmp(model_stop_at(PAGE_TRAINS, 0)->id, "TR1") == 0); assert(model_stop_count(PAGE_TRANSIT) == 0); assert(model_state() == STATE_OK); msg_request_refresh(PAGE_TRANSIT);}
static void test_lost_begin_ignores_followup_stops(void) { send_stop(0, 1, "BASE", "B"); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "BASE") == 0); send_stop(1, 3, "ORPHAN1", "O"); send_stop(2, 3, "ORPHAN2", "O"); model_set_state(STATE_LOADING); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "BASE") == 0); assert(model_state() == STATE_LOADING);}
static void test_stale_staging_cleared_by_late_terminal(void) { msg_request_refresh(PAGE_TRANSIT); send_stop(0, 1, "KEEP4", "K"); send_done_ok(); send_stop(0, 2, "A0", "A"); msg_request_refresh(PAGE_TRAINS); send_done_ok(); send_stop_with_page(PAGE_TRAINS, "B0"); send_done_with_page(PAGE_TRAINS, 0); assert(model_stop_count(PAGE_TRAINS) == 1); assert(strcmp(model_stop_at(PAGE_TRAINS, 0)->id, "B0") == 0); assert(model_stop_count(PAGE_TRANSIT) == 0); msg_request_refresh(PAGE_TRANSIT);}
static void test_lossy_polls_do_not_merge(void) { send_stop(0, 1, "SOLID", "S"); send_done_ok(); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "SOLID") == 0); send_stop(0, 2, "L0", "L"); model_set_state(STATE_LOADING); send_done_ok(); send_stop(1, 2, "L1", "L"); send_done_ok(); assert(model_stop_count(PAGE_TRANSIT) == 1); assert(strcmp(model_stop_at(PAGE_TRANSIT, 0)->id, "SOLID") == 0); assert(model_state() == STATE_LOADING);}
static uint32_t g_seed = 0xC0FFEE42;
static uint32_t rnd(void) { g_seed ^= g_seed << 13; g_seed ^= g_seed >> 17; g_seed ^= g_seed << 5; return g_seed;}
static int32_t rnd_int(void) { switch (rnd() % 5) { case 0: return INT32_MIN; case 1: return INT32_MAX; case 2: return -(int32_t)(rnd() % 1000); case 3: return (int32_t)(rnd() % 32); default: return (int32_t)rnd(); }}
static void rnd_string(char *buf, size_t max) { size_t len = rnd() % max; for (size_t i = 0; i < len; i++) { buf[i] = (char)(1 + rnd() % 255); } buf[len] = '\0';}
static void stop_invariants(const Stop *stop) { assert(strlen(stop->id) < ID_LEN); assert(strlen(stop->name) < NAME_LEN); assert(stop->row_count <= MAX_ROWS); for (uint8_t j = 0; j < MAX_ROWS; j++) { assert(strlen(stop->rows[j].line) < LINE_LEN); assert(strlen(stop->rows[j].destination) < NAME_LEN); assert(strlen(stop->rows[j].platform) < PLATFORM_LEN); }}
static void model_invariants(void) { for (int p = 0; p < PAGE_COUNT; p++) { assert(model_stop_count((Page)p) <= MAX_STOPS); for (uint8_t i = 0; i < MAX_STOPS; i++) { Stop *stop = model_stop_at((Page)p, i); if (stop) { stop_invariants(stop); } } } for (uint8_t i = 0; i < MAX_STOPS; i++) { const Stop *stage = model_staging_stop_at(i); if (stage) { stop_invariants(stage); } } LineDetail *detail = model_line_detail(); assert(strlen(detail->line) < LINE_LEN); assert(strlen(detail->destination) < NAME_LEN); assert(strlen(detail->disruption) < sizeof(detail->disruption)); assert(detail->strip_count <= MAX_STRIP); assert(detail->shape_len <= MAX_SHAPE_BYTES); for (uint8_t i = 0; i < MAX_STRIP; i++) { assert(strlen(detail->strip[i].name) < NAME_LEN); }}
static void test_stress_random_dicts(void) { static const uint32_t int_keys[] = { MESSAGE_KEY_STOP_INDEX, MESSAGE_KEY_STOP_COUNT, MESSAGE_KEY_STOP_DISTANCE, MESSAGE_KEY_ROW_KIND, MESSAGE_KEY_ROW_COLOR, MESSAGE_KEY_ROW_TEXT_COLOR, MESSAGE_KEY_ROW_MIN1, MESSAGE_KEY_ROW_MIN2, MESSAGE_KEY_ERROR, MESSAGE_KEY_CFG_VIBRATE, MESSAGE_KEY_CFG_LANG, MESSAGE_KEY_LINE_STATUS, MESSAGE_KEY_VILLO_MECHANICAL, MESSAGE_KEY_VILLO_ELECTRICAL, MESSAGE_KEY_VILLO_DOCKS, MESSAGE_KEY_PAGE }; static const uint32_t str_keys[] = { MESSAGE_KEY_STOP_ID, MESSAGE_KEY_STOP_NAME, MESSAGE_KEY_ROW_LINE, MESSAGE_KEY_ROW_DEST, MESSAGE_KEY_LINE_DISRUPTION }; static const uint32_t data_keys[] = { MESSAGE_KEY_LINE_SHAPE, MESSAGE_KEY_BOARD_ROWS, MESSAGE_KEY_LINE_STRIPS }; char buf[300]; uint8_t data[256]; for (int iter = 0; iter < 5000; iter++) { TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, (int32_t)(rnd() % 9)); uint16_t extra = (uint16_t)(rnd() % 8); for (uint16_t k = 0; k < extra; k++) { switch (rnd() % 3) { case 0: td_int(&d, int_keys[rnd() % (sizeof(int_keys) / sizeof(int_keys[0]))], rnd_int()); break; case 1: rnd_string(buf, sizeof(buf) - 1); td_str(&d, str_keys[rnd() % (sizeof(str_keys) / sizeof(str_keys[0]))], buf); break; default: { uint16_t len = (uint16_t)(rnd() % 256); for (uint16_t b = 0; b < len; b++) { data[b] = (uint8_t)rnd(); } td_data(&d, data_keys[rnd() % (sizeof(data_keys) / sizeof(data_keys[0]))], data, len); break; } } } td_send(&d); model_invariants(); }}
static void test_update_masks(void) { g_updates = 0; uint8_t blob[2 + ROW_BLOB_ENTRY] = { 2, 1 }; blob_row(blob, 0, "7", 0, 0, "X", 1, -1, "", 0); send_stop_v2(0, 1, "S", "N", blob, sizeof(blob)); assert(g_updates == 0); TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 3); td_int(&d, MESSAGE_KEY_ERROR, 0); td_send(&d); assert(g_updates == 1 && g_masks[0] == MSG_UPDATE_BOARD); td_int(&d, MESSAGE_KEY_KIND, 6); td_str(&d, MESSAGE_KEY_ROW_LINE, "62"); td_send(&d); assert(g_updates == 2 && g_masks[1] == MSG_UPDATE_LINE); td_int(&d, MESSAGE_KEY_KIND, 5); td_int(&d, MESSAGE_KEY_CFG_VIBRATE, 1); td_send(&d); assert(g_updates == 3 && g_masks[2] == MSG_UPDATE_CONFIG);}
static void test_fav_toggle_roundtrip(void) { int sends = pebble_stub_outbox_sends; pebble_stub_cstring_writes = 0; msg_request_fav_toggle("6419", "BOTANIQUE"); assert(pebble_stub_outbox_sends == sends + 1); assert(pebble_stub_cstring_writes == 2); assert(pebble_stub_cstring_keys[0] == MESSAGE_KEY_REQ_STOP_ID); assert(pebble_stub_cstring_keys[1] == MESSAGE_KEY_STOP_NAME); TestDict d = {0}; td_int(&d, MESSAGE_KEY_KIND, 8); td_int(&d, MESSAGE_KEY_FAV_RESULT, 1); td_send(&d); assert(g_fav_result_count == 1 && g_fav_results[0] == 1); td_int(&d, MESSAGE_KEY_KIND, 8); td_send(&d); assert(g_fav_result_count == 1); td_int(&d, MESSAGE_KEY_KIND, 8); td_int(&d, MESSAGE_KEY_FAV_RESULT, 3); td_send(&d); assert(g_fav_result_count == 2 && g_fav_results[1] == 3);}
int main(void) { model_init(); msg_init(on_update); assert(pebble_stub_inbox != NULL); test_stop_and_villo_fill(); test_villo_absent_defaults(); test_board_rows_blob_fill(); test_board_rows_blob_bounds(); test_board_rows_wrong_version(); test_board_rows_short_blob(); test_line_strips_blob(); test_line_strips_blob_bounds(); test_string_termination(); test_int32_extremes(); test_pseudo_prefix_ids_stay_distinct(); test_done_message_states(); test_kind3_stamps_arrival_for_staleness(); test_error_message_states(); test_vibrate_on_zero(); test_shape_byte_caps(); test_ignores_incomplete_dicts(); test_page_routes_stops_and_defaults_to_transit(); test_out_of_range_page_does_not_corrupt_model(); test_partial_board_never_commits(); test_late_old_page_reply_cannot_evict_request(); test_lost_begin_ignores_followup_stops(); test_stale_staging_cleared_by_late_terminal(); test_lossy_polls_do_not_merge(); test_stress_random_dicts(); test_update_masks(); test_fav_toggle_roundtrip(); assert(g_updates > 0); int sends_before = pebble_stub_outbox_sends; msg_request_refresh(PAGE_TRANSIT); pebble_stub_cstring_writes = 0; msg_request_line("6419", "62", ""); assert(pebble_stub_cstring_writes == 2); pebble_stub_cstring_writes = 0; msg_request_line("6419", "62", "SAINTE-ANNE"); assert(pebble_stub_cstring_writes == 3); assert(pebble_stub_cstring_keys[2] == MESSAGE_KEY_REQ_DEST); assert(pebble_stub_outbox_sends == sends_before + 3); return 0;}