1. Тема дня

Design by Contract формализует правила системы прямо в коде:

text
precondition: что caller обязан выполнить
postcondition: что функция гарантирует после успеха
invariant: что всегда истинно в стабильном состоянии объекта

Главная мысль: assertion не проверяет внешний мир; asser- tion ловит нарушение внутреннего обещания программы.

2. Зачем это нужно

Для EC25 invariant:

text
transaction_active == false -> active_command == NONE
transaction_active == true -> transaction_id != 0

Для phase FSM:

text
RED && GREEN -> quality == CONFLICT
input_stale -> quality != VALID
off_threshold < on_threshold

Для DMA:

text
CPU_OWNED и DMA_OWNED не могут быть истинны одновременно

Один invariant checker используется в production, unit tests, fuzzing и replay.

3. Теория

Четыре класса ошибок

A. Недоверенный внешний input:

text
неверный MQTT payload, CRC error CAN, слишком длинная AT-строка

Реакция: reject/error/resync, не assert. B. Ожидаемый runtime failure:

text
EC25 не зарегистрировался, DNS timeout, CAN bus-off

Реакция: FSM recovery, retry/backoff. C. Нарушение внутреннего контракта:

text
double free buffer, ONLINE без registration, retry_count > limit

Реакция: contract fault. D. Потеря безопасного состояния:

text
невозможно гарантировать состояние output или корректность phase data

Реакция: safe-state, latch fault, затем controlled recovery/reboot.

Invariant checker

c
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

c
_Static_assert(SPSC_CAPACITY != 0U &&
               (SPSC_CAPACITY & (SPSC_CAPACITY - 1U)) == 0U,
               "SPSC capacity must be a nonzero power of two");
c
_Static_assert((SPSC_CAPACITY & (SPSC_CAPACITY - 1U)) == 0U,
               "SPSC capacity must be power of two");

Assertions без side effects

Плохо:

c
assert(queue_pop(&queue, &item));

Правильно:

c
bool popped = queue_pop(&queue, &item);
APP_INVARIANT(popped);

Fail-fast не означает instant reboot

Правильный path:

text
contract violation -> capture minimal state -> safe-state -> fault record -> controlled abort/reboot or local recovery

4. Типичные ошибки

  1. Assertion на внешний MQTT/UART input.
  2. ESP_ERROR_CHECK() на сетевой timeout.
  3. Продолжать выполнение после should never happen.
  4. Assertion с side effects.
  5. Только ptr != NULL, без логических invariants.
  6. Разные invariant-функции для production/fuzz/replay.
  7. Один гигантский invariant без конкретного violation ID.
  8. Брать mutex или писать NVS из ISR contract handler.
  9. Дорогой O(N) invariant в каждом ADC sample.
  10. Полностью отключить critical checks в release.
  11. Считать watchdog заменой assertions.
  12. Считать assertions заменой normal error handling.
  13. 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:

c
_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.
Какой случай является нарушением внутреннего контракта, а не внешним input или ожидаемым runtime failure?

Задание

Обработчик 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.