1. Тема дня
Design by Contract формализует правила системы прямо в коде:
precondition: что caller обязан выполнить
postcondition: что функция гарантирует после успеха
invariant: что всегда истинно в стабильном состоянии объектаГлавная мысль: assertion не проверяет внешний мир; asser- tion ловит нарушение внутреннего обещания программы.
2. Зачем это нужно
Для EC25 invariant:
transaction_active == false -> active_command == NONE
transaction_active == true -> transaction_id != 0Для phase FSM:
RED && GREEN -> quality == CONFLICT
input_stale -> quality != VALID
off_threshold < on_thresholdДля DMA:
CPU_OWNED и DMA_OWNED не могут быть истинны одновременноОдин invariant checker используется в production, unit tests, fuzzing и replay.
3. Теория
Четыре класса ошибок
A. Недоверенный внешний input:
неверный MQTT payload, CRC error CAN, слишком длинная AT-строкаРеакция: reject/error/resync, не assert. B. Ожидаемый runtime failure:
EC25 не зарегистрировался, DNS timeout, CAN bus-offРеакция: FSM recovery, retry/backoff. C. Нарушение внутреннего контракта:
double free buffer, ONLINE без registration, retry_count > limitРеакция: contract fault. D. Потеря безопасного состояния:
невозможно гарантировать состояние output или корректность phase dataРеакция: safe-state, latch fault, затем controlled recovery/reboot.
Invariant checker
typedef enum {
MODEM_INV_OK = 0,
MODEM_INV_NULL,
MODEM_INV_COMMAND_WITHOUT_TRANSACTION,
MODEM_INV_MQTT_WITHOUT_ONLINE_MODEM,
MODEM_INV_RETRY_OVERFLOW,
} modem_invariant_result_t;
static modem_invariant_result_t modem_invariants_check(const modem_core_t *m)
{
if (m == NULL) return MODEM_INV_NULL;
if (!m->transaction_active && m->active_command != MODEM_COMMAND_NONE)
return MODEM_INV_COMMAND_WITHOUT_TRANSACTION;
if (m->mqtt_state == MQTT_STATE_CONNECTED && m->state != MODEM_STATE_ONLINE)
return MODEM_INV_MQTT_WITHOUT_ONLINE_MODEM;
if (m->retry_count > m->policy.retry_limit)
return MODEM_INV_RETRY_OVERFLOW;
return MODEM_INV_OK;
}Проверять до и после handle_event().
Compile-time contracts
_Static_assert(SPSC_CAPACITY != 0U &&
(SPSC_CAPACITY & (SPSC_CAPACITY - 1U)) == 0U,
"SPSC capacity must be a nonzero power of two");_Static_assert((SPSC_CAPACITY & (SPSC_CAPACITY - 1U)) == 0U,
"SPSC capacity must be power of two");Assertions без side effects
Плохо:
assert(queue_pop(&queue, &item));Правильно:
bool popped = queue_pop(&queue, &item);
APP_INVARIANT(popped);Fail-fast не означает instant reboot
Правильный path:
contract violation -> capture minimal state -> safe-state -> fault record -> controlled abort/reboot or local recovery4. Типичные ошибки
- Assertion на внешний MQTT/UART input.
- ESP_ERROR_CHECK() на сетевой timeout.
- Продолжать выполнение после should never happen.
- Assertion с side effects.
- Только ptr != NULL, без логических invariants.
- Разные invariant-функции для production/fuzz/replay.
- Один гигантский invariant без конкретного violation ID.
- Брать mutex или писать NVS из ISR contract handler.
- Дорогой O(N) invariant в каждом ADC sample.
- Полностью отключить critical checks в release.
- Считать watchdog заменой assertions.
- Считать assertions заменой normal error handling.
- default: break для невозможного enum state.
5. Практическое задание
Внедри modem_invariants_check() и вызывай его на входе/выходе modem event handler. Добавь intentional bug: при AT_OK сбрось transaction_active=false, но не сбрасывай active_command. Invariant должен сразу дать MODEM_INV_COMMAND_WITHOUT_TX. Добавь _Static_assert для buffer sizes:
_Static_assert(MODEM_MAX_AT_COMMAND_LENGTH < MODEM_TX_BUFFER_SIZE,
"AT command can overflow TX buffer");Затем подключи тот же checker в fuzz target и replay runner.
6. Что попробовать дальше
- Разделить contract levels: critical always-on, normal debug, deep audit.
- В production fault record сохранять contract ID, state, transac- tion ID, config generation, boot ID.
- Для remote command сделать разные типы: raw_command_t, verified_command_t, authorized_command_t.
Задание
Обработчик AT_OK сбросил transaction_active, но оставил active_command. Какой outcome checker ожидается в уроке? Опишите fault path без assertion с side effects.
Критерии самопроверки: Назовите violation и отделите diagnostics/safe-state действия от проверки без side effects.
Показать ответ автора
Checker сообщает MODEM_INV_COMMAND_WITHOUT_TX. Сохраните минимальный контекст, перейдите в safe-state, запишите fault, затем выполните controlled abort/reboot или local recovery по fault policy. Изменения состояния выполняются вне assertion; один checker используется в production, fuzzing и replay.