Перейти к основному содержимому

Фреймворки на коде

Проверка сценариями

Создайте элемент, запустите действия и проверьте каждое значение и каждую запись.

Что содержит сценарий

Сценарий это JSON-файл в scenarios/. Он создаёт элемент из входных данных, по порядку запускает действия и проверяет результаты. Манифест перечисляет все файлы сценариев, и в каждом файле один сценарий.

scenarios/heal.json
{
  "id": "heal-after-damage",
  "framework": "keeper",
  "parameters": { "name": "Mira", "max_hp": 10 },
  "expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 },
  "actions": [
    {
      "action": "hurt",
      "parameters": { "amount": 6 },
      "expectedWrites": [{ "op": "ledger.add", "key": "hp", "value": -6 }]
    },
    {
      "action": "heal",
      "parameters": { "amount": 9 },
      "expectedWrites": [{ "op": "ledger.set", "key": "hp", "value": 10 }]
    },
    { "action": "heal", "parameters": { "amount": 3 }, "expectedWrites": [] }
  ]
}
  1. Строки 4-5 Создайте элемент из этих входных данных и проверьте его начальные значения.
  2. Строка 6 actions перечисляет шаги для запуска по порядку.
  3. Строка 10 Точные записи, которые должен сделать шаг.
  4. Строка 17 Записей нет: ничего не изменилось.
ПолеЗначение
idНазвание сценария.
frameworkИдентификатор фреймворка, который нужно создать.
parametersВходные данные для создания.
expectedValuesВсе хранимые значения сразу после создания.
expectedChildrenЗначения каждого дочернего слота.
actionsШаги для запуска, не больше 32 на сценарий. У каждого есть действие, его параметры и expectedWrites.
ledgerПроверки производных значений.
listsЗаполняет список записями по идентификатору пресета.
expectedListsЗаписи, которые каждый список содержит в конце, по идентификатору пресета.
poolRollsЗаписанные броски для пула с методом roll по идентификатору пула. См. «Брошенные значения».
contentЗаписи контента, закреплённые за построенным Element (kind, entryId, name и parameters); попадают в релиз так же, как закрепления персонажа.

Шаг может использовать и другие поля:

Поле шагаЗначение
chooseДля действия списка: запись, которую игрок выбирает для каждого входного значения ContentRef, по идентификатору пресета.
expectedRefusalОтказ, которым должен закончиться шаг. Тогда шаг не ждёт записей.
expectedValuesAfterХранимые и производные значения после шага. Производные значения сначала пересчитываются.

Записи

expectedWrites перечисляет по порядку все записи, которые делает шаг. Используйте ledger.set для установленного значения и ledger.add для добавленной величины. Шаг, который ничего не меняет, не перечисляет записей, поэтому проверяется и условие, которое пропускает запись.

Если настоящие записи отличаются, проверка покажет оба списка.

"expectedWrites": [{ "op": "ledger.set", "key": "hp", "value": 9 }]
Expected ordered writes [{"op":"ledger.set","key":"hp","value":9}]; observed [{"op":"ledger.set","key":"hp","value":10}].

Производные значения и дочерние элементы

Список ledger проверяет функцию журнала. В нём указаны идентификатор функции, исходное значение и значение, которое она должна вернуть. expectedChildren проверяет дочерние элементы.

scenarios/new-keeper.json
  "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } },
  1. Строка 17 Проверяет элементы в дочерних слотах.
scenarios/new-keeper.json
  "ledger": [{ "function": "guard", "currentValue": 0, "expected": 3 }]
  1. Строка 18 Вызывает функцию защиты с текущим значением 0 и ожидает 3.

Что нужно покрыть

  • Для каждого конструктора, действия и функции журнала нужен сценарий.
  • Каждому действию нужен хотя бы один шаг. Добавьте шаг для каждой ветки, которую хотите проверить, например один, который пишет, и один, который ничего не пишет.
  • Действию, которое добавляет в список, нужны один принятый шаг и один шаг с expectedRefusal, что бы ни говорило его предложение. Отклонённый шаг доказывает, что предложение умеет отказывать. Действию, которое только удаляет из списка, нужен один принятый шаг.
  • Ожидаемые значения должны совпадать с тем, что конструктор действительно возвращает, когда функции журнала уже применены.

Шаги называются по своему сценарию. Сообщение вроде heal-after-damage-2 означает третий шаг сценария heal-after-damage, потому что число считается с 0. В других сообщениях шаги считаются с 1.

Шаг, который ждёт отказа от действия, которое не было отклонено, не проходит проверку. То же верно и наоборот:

the step expects it to be refused with NOT_OFFERED
Add expectedRefusal

Действие без шагов даёт ошибку:

SELF_SCALAR_ACTION_SCENARIO_COVERAGE_MISSING: action keeper.hurt (Take damage) has no executable scenario steps.

Действие со списком, у которого нет принятого или отклонённого шага, даёт ошибку:

add a step with expectedRefusal, so its refused branch is proven.
add a step whose list.add is accepted, so its accepted branch is proven.

Ожидаемые значения, которые отличаются от значений конструктора, дают ошибку:

Constructor output does not match its scenario.

Отказы

Действие со списком может быть отклонено. Шаг, который это ожидает, называет код в expectedRefusal и не перечисляет записей.

КодКогда
NOT_OFFEREDВыбранной записи нет среди записей, которые допускает предложение.
ALREADY_IN_LISTСписок уже содержит эту запись.
LIST_FULLСписок достиг максимального размера.
NOT_IN_LISTlist.remove называет запись, которой нет в списке.
LIST_CAPБудет превышен лимит, объявленный списком.
SAME_NAMEВ списке уже есть запись с таким названием.

Этот проект проверяет несколько из них. В нём есть и шаг, где проверка останавливает действие, поэтому шаг не ждёт ни записей, ни отказа.

scenarios/pack.json
{
  "id": "pack-trinkets",
  "framework": "keeper",
  "parameters": { "name": "Mira", "max_hp": 10 },
  "expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 },
  "actions": [
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [{ "op": "list.add", "key": "pack", "value": "rope" }]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "ALREADY_IN_LIST",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "anvil" },
      "expectedRefusal": "NOT_OFFERED",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "candle" },
      "expectedWrites": [
        { "op": "list.add", "key": "pack", "value": "candle" }
      ]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "bell" },
      "expectedWrites": []
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [
        { "op": "list.remove", "key": "pack", "value": "rope" }
      ]
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "NOT_IN_LIST",
      "expectedWrites": []
    }
  ],
  "expectedLists": { "pack": ["candle"] }
}
  1. Строки 7-12 Принято: list.add записывает верёвку.
  2. Строки 13-19 Отказ: список уже содержит верёвку.
  3. Строки 20-26 Отказ: предложение не допускает наковальню.
  4. Строки 35-40 В рюкзаке уже две записи, поэтому проверка ничего не пишет и отказа не ждут.
  5. Строки 41-48 Удалено: верёвка уходит из списка.
  6. Строки 49-55 Отказ: верёвки больше нет в списке.
  7. Строка 57 В конце в рюкзаке остаётся только свеча.

Запуск

«Проверить и показать» в The Forge компилирует проект и запускает каждый сценарий. Ничего не публикуется, и действующие персонажи не меняются.

Дальше