aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/module-save.c12
-rw-r--r--core/module.c69
-rw-r--r--core/module.h10
-rw-r--r--core/test-module.c91
4 files changed, 162 insertions, 20 deletions
diff --git a/core/module-save.c b/core/module-save.c
index 1300832..7d9d951 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -260,7 +260,8 @@ static int read_outputs(const unsigned char *at, size_t size, SentShape *shapes,
static PureList *stored_list(const PureApp *app, uint32_t i) {
if (i == PURE_LIST_HEARD) return app->heard;
if (i == PURE_LIST_OUTBOX) return app->outbox;
- return i < app->module->states && app->module->state_type[i] == PURE_STATE_LIST ? app->lists[i] : NULL;
+ return i < app->module->states && app->module->state_type[i] == PURE_STATE_LIST &&
+ !pure_state_transient(app->module, i) ? app->lists[i] : NULL;
}
static const char *stored_name(const PureApp *app, uint32_t i) {
@@ -696,7 +697,7 @@ static size_t encode_values(const PureModule *m, const PureValue *values, unsign
size_t at = 4;
memcpy(out, values_magic, 4);
for (uint32_t i = 0; i < m->states; ++i) {
- if (!scalar(m->state_type[i])) continue;
+ if (!scalar(m->state_type[i]) || pure_state_transient(m, i)) continue;
size_t length = strlen(m->state_names[i]);
uint64_t bits;
memcpy(&bits, &values[i], sizeof(bits));
@@ -715,7 +716,7 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, Pu
PureDraft *const *texts, PureList *const *lists) {
bool scalars = false, fresh = false;
for (uint32_t i = 0; i < m->states; ++i)
- if (scalar(m->state_type[i])) {
+ if (scalar(m->state_type[i]) && !pure_state_transient(m, i)) {
scalars = true;
fresh = fresh || kept[i] < 0;
}
@@ -734,7 +735,7 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, Pu
for (unsigned b = 0; b < 8; ++b) bits |= (uint64_t)bytes[at + b] << (8 * b);
at += 8;
int state = pure_module_state(m, name);
- if (state < 0 || kept[state] >= 0 || !scalar(type)) continue;
+ if (state < 0 || kept[state] >= 0 || !scalar(type) || pure_state_transient(m, (uint32_t)state)) continue;
/* A number, a bool or an i64, each as the state's type holds it when it fits (pure_cell_as); a position where it fits. */
uint8_t from = pure_value_kind(type), to = pure_value_kind(m->state_type[state]);
PureCell cell = {.i64 = (int64_t)bits};
@@ -1016,7 +1017,8 @@ PureResult pure_save_commit(PureApp *app, const PureValue *values, void *const *
uint32_t states[2 * PURE_MODULE_STATES + 3];
size_t count = 0;
for (uint32_t i = 0; i < m->states; ++i) {
- PureDraft *draft = m->state_type[i] == PURE_STATE_TEXT ? app->texts[i] :
+ PureDraft *draft = pure_state_transient(m, i) ? NULL :
+ m->state_type[i] == PURE_STATE_TEXT ? app->texts[i] :
m->state_type[i] == PURE_STATE_LIST ? app->list_drafts[i] : NULL;
if (draft && !same_version(pure_draft_version(draft), app->saved[i])) {
states[count] = i;
diff --git a/core/module.c b/core/module.c
index f3fbcd2..6df93bc 100644
--- a/core/module.c
+++ b/core/module.c
@@ -1035,8 +1035,10 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
const unsigned char *text = p + size - constant_bytes - strings;
if (constant_bytes) memcpy(out->constant_data, p + size - constant_bytes, constant_bytes);
for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
- out->state_type[i] = (uint8_t)read32(at + 4);
- if (read32(at + 4) > PURE_STATE_I64) REFUSE("state type", i);
+ uint32_t type = read32(at + 4);
+ out->state_type[i] = (uint8_t)type;
+ if ((type & ~(uint32_t)PURE_STATE_TRANSIENT) > PURE_STATE_I64) REFUSE("state type", i);
+ if (type & PURE_STATE_TRANSIENT) out->transient |= UINT32_C(1) << i;
/* A number's, a bool's or an i64's value is its 8 bytes; others have flags:
* a mark (cursor), a keyed list's key field + 1, or a list of a message's rows. */
bool value = out->state_type[i] == PURE_STATE_NUM || out->state_type[i] == PURE_STATE_BOOL ||
@@ -1044,6 +1046,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
uint32_t flags = value ? 0 : read32(at + 12);
bool cursor = out->state_type[i] == PURE_STATE_CURSOR, list = out->state_type[i] == PURE_STATE_LIST;
bool rows = list && flags == PURE_LIST_ROWS;
+ if (rows && type & PURE_STATE_TRANSIENT) REFUSE("state type", i); /* an event's own, saved or not */
/* A list of a message's rows is named by the field's path (input.field), no other state. */
if (!name_at(text, strings, read32(at), rows ? NAME_PATH : NAME_IDENTIFIER, out->state_names[i]) ||
(rows && !strchr(out->state_names[i], '.')))
@@ -1182,6 +1185,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_BOOL))) ||
(out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST)))
REFUSE("state reference", i);
+ /* A stored position into what is not stored would come back meaning nothing. */
+ for (uint32_t i = 0; i < out->states; ++i)
+ if ((out->state_type[i] == PURE_STATE_CURSOR || out->state_type[i] == PURE_STATE_INDEX) &&
+ !(out->transient >> i & 1) && out->transient >> out->state_ref[i] & 1)
+ REFUSE("stored position in a transient state", i);
/* A table names its list in place of code; its columns follow it. */
for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->view_names[i])) REFUSE("view name", i);
@@ -2791,6 +2799,30 @@ PURE_COLD static PureResult new_text(PureDraft **out, const PureModule *m, uint3
return result;
}
+/* A stored draft given a transient text's bytes, to be saved (saved: none yet). */
+PURE_COLD static PureResult copy_text(PureDraft *to, const PureDraft *from, PureVersion *saved) {
+ size_t size = pure_draft_size(from);
+ unsigned char *bytes = size ? malloc(size) : NULL;
+ if (size && !bytes) return PURE_ALLOCATION;
+ Copy copy = {bytes};
+ PureResult result = size ? pure_draft_read(from, 0, size, copy_out, &copy) : PURE_OK;
+ if (result == PURE_OK) result = pure_draft_splice(to, 0, pure_draft_size(to), (PureSpan){bytes, size});
+ free(bytes);
+ *saved = (PureVersion){UINT64_MAX, UINT64_MAX};
+ return result;
+}
+
+/* A draft in memory with a text's bytes, or NULL. */
+static PureDraft *text_copy(const PureDraft *from) {
+ PureDraft *copy = pure_draft_new();
+ PureVersion unused;
+ if (copy && copy_text(copy, from, &unused) != PURE_OK) {
+ pure_draft_close(copy);
+ copy = NULL;
+ }
+ return copy;
+}
+
PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record) {
const PureRecord *r = &m->record[record];
uint8_t kinds[PURE_RECORD_FIELDS];
@@ -3674,7 +3706,10 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) {
memcpy(made->values, app->values, sizeof(made->values));
bool ok = true;
for (uint32_t i = 0; i < m->states && ok; ++i) {
- if (app->texts[i]) ok = (made->texts[i] = pure_draft_freeze(app->texts[i])) != NULL;
+ /* A stored text is shared as its store holds it; a transient one, never stored, is copied. */
+ if (app->texts[i])
+ ok = (made->texts[i] = pure_state_transient(m, i) ? text_copy(app->texts[i]) : pure_draft_freeze(app->texts[i])) !=
+ NULL;
if (!ok || !app->lists[i]) continue;
if (last && last->lists[i] && pure_list_current(last->lists[i], app->lists[i])) {
pure_list_hold(last->lists[i]);
@@ -3921,22 +3956,29 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
bool valued = pure_value_kind(was) != 0xFF && pure_value_kind(now) != 0xFF;
kept[i] = previous >= 0 && (was == now || valued) ? previous : -1;
if (kept[i] >= 0) saved[i] = app->saved[kept[i]];
+ /* A transient state is never stored (018): one that was stored is now as a dropped one is. */
+ bool transient = pure_state_transient(module, i), was_transient = kept[i] >= 0 && pure_state_transient(old, kept[i]);
if (module->state_type[i] == PURE_STATE_TEXT) {
- if (kept[i] >= 0) texts[i] = app->texts[kept[i]];
+ if (kept[i] >= 0 && (transient || !was_transient || !stored)) texts[i] = app->texts[kept[i]];
+ else if (result == PURE_OK && kept[i] >= 0 && /* a text stored from now on: its draft in the store holds it */
+ (result = pure_save_open_text(app, module, i, &texts[i], &saved[i])) == PURE_OK)
+ result = copy_text(texts[i], app->texts[kept[i]], &saved[i]);
else if (result == PURE_OK)
- result = stored ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) :
+ result = stored && !transient ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) :
new_text(&texts[i], module, module->state_ref[i]);
} else if (module->state_type[i] == PURE_STATE_LIST) {
if (result != PURE_OK) continue;
bool rows = module->state_flags[i] & PURE_LIST_ROWS >> 8; /* rows are never stored */
+ bool saving = lists_stored && !rows && !transient;
if (kept[i] >= 0) {
result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module,
module->state_ref[i]);
list_drafts[i] = app->list_drafts[kept[i]];
- listed[i] = (PureSaved){app->image_seq[kept[i]], app->logged_since[kept[i]], app->logged[kept[i]], false};
- /* Its record changed: its image, in the new fields, holds it before any change is logged. */
- rewrite[i] = result == PURE_OK && lists_stored && !rows && lists[i] != app->lists[kept[i]];
- } else if (lists_stored && !rows) {
+ if (!transient && !was_transient)
+ listed[i] = (PureSaved){app->image_seq[kept[i]], app->logged_since[kept[i]], app->logged[kept[i]], false};
+ /* Its record changed, or it is stored from now on: its image, in the new fields, holds it before any change is logged. */
+ rewrite[i] = result == PURE_OK && saving && (lists[i] != app->lists[kept[i]] || was_transient);
+ } else if (saving) {
result = pure_save_open_list(app, module, i, &lists[i], &list_drafts[i], &listed[i]);
rewrite[i] = result == PURE_OK && listed[i].rewrite;
} else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION;
@@ -3963,7 +4005,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
}
if (kept[i] < 0 || lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]);
lists[i] = sorted;
- rewrite[i] = lists_stored && !(module->state_flags[i] & PURE_LIST_ROWS >> 8);
+ rewrite[i] = lists_stored && !(module->state_flags[i] & PURE_LIST_ROWS >> 8) && !pure_state_transient(module, i);
}
/* A cursor or index keeps its position only while its text or list is the
* one it belonged to. */
@@ -3999,7 +4041,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
bool imaged = result == PURE_OK;
for (uint32_t j = 0; result == PURE_OK && old && j < old->states; ++j) {
bool dropped = old->state_type[j] == PURE_STATE_LIST && app->logged_since[j];
- for (uint32_t i = 0; dropped && i < module->states; ++i) dropped = kept[i] != (int)j;
+ for (uint32_t i = 0; dropped && i < module->states; ++i) dropped = kept[i] != (int)j || pure_state_transient(module, i);
if (dropped) result = pure_save_image(app, old, j, app->lists[j]);
}
for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i)
@@ -4022,6 +4064,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
for (uint32_t i = 0; i < module->states; ++i) {
if (kept[i] >= 0) {
if (lists[i] && lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]);
+ if (texts[i] && texts[i] != app->texts[kept[i]]) pure_draft_close(texts[i]);
continue;
}
pure_draft_close(texts[i]);
@@ -4063,9 +4106,9 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
}
pure_save_release(app);
for (uint32_t i = 0; i < module->states; ++i) {
- /* A stored list's changes are recorded from now on. */
+ /* A stored list's changes are recorded from now on; a transient one's never. */
if (lists_stored && module->state_type[i] == PURE_STATE_LIST && !(module->state_flags[i] & PURE_LIST_ROWS >> 8))
- pure_list_journal(lists[i], true);
+ pure_list_journal(lists[i], !pure_state_transient(module, i));
/* Without memory for its keys a keyed list is still searched, only slower. */
if (module->state_type[i] == PURE_STATE_LIST)
(void)pure_list_keyed(lists[i], module->state_key[i] ? (int)module->state_key[i] - 1 : -1);
diff --git a/core/module.h b/core/module.h
index 162a8b1..d0a82bc 100644
--- a/core/module.h
+++ b/core/module.h
@@ -144,6 +144,12 @@ typedef union { double num; int64_t i64; } PureValue;
* message's rows field fills it as the message arrives, it is emptied when
* every event ends, and it is never saved nor logged for undoing. */
enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 };
+/* A transient state (bit 8 of its type word in a module, a bit of
+ * PureModule.transient): the app holds it while it runs, and undoes it with
+ * a failed event, but never saves it: each start begins it at its initial
+ * value (docs/design/018). A stored cursor or index is of a stored text or
+ * list. */
+enum { PURE_STATE_TRANSIENT = 256 };
/* A table view has one row per item of a list, or per item its filter
* keeps; its columns are number, bool, i64 or text views evaluated for one
* row at a time. */
@@ -217,6 +223,7 @@ typedef struct {
uint8_t state_type[PURE_MODULE_STATES];
uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */
uint8_t state_flags[PURE_MODULE_STATES]; /* cursor: PURE_CURSOR_STAYS; list: PURE_LIST_ROWS >> 8 */
+ uint32_t transient; /* a bit per transient state */
/* A keyed list: its key field + 1 (0: none). Its items stay in key order
* (numbers by value, texts by bytes), each key once, and keys do not change. */
uint8_t state_key[PURE_MODULE_STATES];
@@ -248,6 +255,9 @@ typedef struct {
PureValue record_field_default[PURE_MODULE_RECORD_FIELDS]; /* a number (a bool's 0 or 1), an i64, or a text constant's index */
} PureModule;
+/* Whether a state of m is transient (PURE_STATE_TRANSIENT). */
+static inline bool pure_state_transient(const PureModule *m, uint32_t state) { return m->transient >> state & 1; }
+
/* Decode and verify into out, which must be zeroed (a static one is) or hold
* a loaded module: what it held is given back first. On failure out holds
* nothing and error names the rule. */
diff --git a/core/test-module.c b/core/test-module.c
index c91bd2c..2f429f1 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -52,6 +52,7 @@ typedef struct { uint8_t op; uint32_t arg; double value; } O;
* 32-bit two's complement) states follow the number states; text views
* follow the number views. */
typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state type, its flags from bit 8 */
+#define TR (1u << 24) /* in a T's type: a transient state */
/* A record field: a number and its default, text (text = 1) whose default
* is the constant numbered initial, a bool (text = 2) or an i64 (text = 3)
* and its default. */
@@ -130,7 +131,7 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t i = 0; i < spec.ns; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put_double(p, spec.s[i].initial); }
for (size_t i = 0; i < spec.nt; ++i) {
p = put32(p, offset[n++]);
- p = put32(p, spec.t[i].type & 0xFF);
+ p = put32(p, (spec.t[i].type & 0xFF) | (spec.t[i].type & TR ? PURE_STATE_TRANSIENT : 0));
if ((spec.t[i].type & 0xFF) == PURE_STATE_BOOL) { p = put_double(p, spec.t[i].ref); continue; }
if ((spec.t[i].type & 0xFF) == PURE_STATE_I64) {
int64_t initial = (int32_t)spec.t[i].ref;
@@ -139,7 +140,7 @@ static size_t build(unsigned char *out, Spec spec) {
continue;
}
p = put32(p, spec.t[i].ref);
- p = put32(p, spec.t[i].type >> 8);
+ p = put32(p, (spec.t[i].type & ~TR) >> 8);
}
for (size_t i = 0; i < spec.nc; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, 1); }
uint32_t field_offsets[64], field_start = 0;
@@ -2045,6 +2046,91 @@ static void stored_migration(void) {
extern unsigned long pure_save_test_checkpoints;
+/* Transient states (018) are the app's while it runs, undone with a failed
+ * event, never stored: a start begins them anew. Replacing the module moves
+ * a state between stored and transient: one stored from then on is saved,
+ * and one no longer stored is to the store as a dropped state. */
+static void transient_states(void) {
+ enum { N, T_, NOTE_, LOG, I_, KEEP, PICK, REQ = PURE_OP_REQUIRE, TSET_ = PURE_OP_TEXT_SET };
+ static const S nums[] = {{"n", 0}};
+ static const T a_states[] = {{"t", 0 | TR, 0}, {"note", 1 | TR, 1}, {"log", 3 | TR, 0}, {"i", 4 | TR, LOG},
+ {"keep", 3, 0}, {"pick", 4 | TR, KEEP}};
+ static const T b_states[] = {{"t", 0, 0}, {"note", 1, 1}, {"log", 3, 0}, {"i", 4 | TR, LOG},
+ {"keep", 3 | TR, 0}, {"pick", 4 | TR, KEEP}};
+ static const T stored_index[] = {{"t", 0 | TR, 0}, {"note", 1 | TR, 1}, {"log", 3 | TR, 0}, {"i", 4, LOG},
+ {"keep", 3, 0}, {"pick", 4 | TR, KEEP}};
+ static const RF fields[] = {{"value", 0, 0}};
+ static const R records[] = {{"row", fields, 1}};
+ static const char *const constants[] = {"", "x"};
+ static const I inputs[] = {KEY("inc", "KeyI"), KEY("fail", "KeyF"), KEY("touch", "KeyT")};
+ static const O code[] = {
+ {L, N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, N, 0}, /* n = n + 1 */
+ {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, /* t = t + 1 */
+ {L, T_, 0}, {LA, LOG, 0}, {L, N, 0}, {LA, KEEP, 0}, /* log.append(row { t }), keep.append(row { n }) */
+ {XE, LOG, 0}, {XS, I_, 0}, {XE, KEEP, 0}, {XS, PICK, 0}, /* i = log.last, pick = keep.last */
+ {TL, NOTE_, 0}, {TK, 1, 0}, {CAT, 0, 0}, {TSET_, NOTE_, 0}, /* note = note + "x" */
+ /* fail: the transient changes, then a require that fails */
+ {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, {L, T_, 0}, {LA, LOG, 0},
+ {TL, NOTE_, 0}, {TK, 1, 0}, {CAT, 0, 0}, {TSET_, NOTE_, 0}, {C, 1, 0}, {REQ, 0, 0},
+ /* touch: only transient states */
+ {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, {L, T_, 0}, {LA, LOG, 0},
+ };
+ static const H handlers[] = {{1, 0, 20}, {3, 20, 12}, {5, 32, 6}};
+ Spec a = {nums, 1, inputs, 3, NULL, 0, handlers, 3, code, LEN(code), NULL, 0, a_states, 6, constants, 2, NULL, 0,
+ records, 1, NULL, 0};
+ Spec b = a, c = a;
+ b.t = b_states;
+ c.t = stored_index;
+ PureModule refused = {0};
+ CHECK(load(c, &refused) != PURE_OK && strstr(error, "stored position in a transient state"));
+ CHECK(load(a, &module_a) == PURE_OK && load(b, &module_b) == PURE_OK);
+ CHECK(pure_state_transient(&module_a, T_) && !pure_state_transient(&module_a, KEEP) && !pure_state_transient(&module_b, T_));
+ char *path = new_store();
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[N].num == 2 && app.values[T_].num == 2 && pure_list_count(app.lists[LOG]) == 2 &&
+ pure_list_count(app.lists[KEEP]) == 2 && app.values[I_].num == 1 && app.values[PICK].num == 1 &&
+ pure_draft_size(app.texts[NOTE_]) == 3);
+ /* A snapshot copies a transient text, and shares its lists as any other. */
+ PureSnapshot *snapshot = pure_app_snapshot(&app);
+ PureApp reader = {0};
+ CHECK(snapshot);
+ pure_app_read(&reader, snapshot);
+ CHECK(pure_draft_size(reader.texts[NOTE_]) == 3 && pure_list_count(reader.lists[LOG]) == 2 && reader.values[T_].num == 2);
+ pure_app_stop(&reader);
+ pure_snapshot_release(snapshot);
+ /* A failed event undoes transient states too. */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) != PURE_OK && app.values[T_].num == 2 &&
+ pure_list_count(app.lists[LOG]) == 2 && pure_draft_size(app.texts[NOTE_]) == 3);
+ /* Changing only transient states stores nothing. */
+ uint64_t seq = app.seq;
+ for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.seq == seq && app.values[T_].num == 5 && pure_list_count(app.lists[LOG]) == 5);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(app.values[N].num == 2 && pure_list_count(app.lists[KEEP]) == 2 && app.values[T_].num == 0 &&
+ !pure_list_count(app.lists[LOG]) && app.values[I_].num == 0 && app.values[PICK].num == 0 &&
+ pure_draft_size(app.texts[NOTE_]) == 1);
+ /* Into b: t, note and log are stored from now on, keep is not. */
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.values[T_].num == 2 && pure_list_count(app.lists[LOG]) == 2 &&
+ pure_list_count(app.lists[KEEP]) == 4 && pure_draft_size(app.texts[NOTE_]) == 3);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK);
+ CHECK(app.values[N].num == 5 && app.values[T_].num == 3 && pure_list_count(app.lists[LOG]) == 3 &&
+ pure_list_get(app.lists[LOG], 2, 0).num == 3 && pure_draft_size(app.texts[NOTE_]) == 4 &&
+ !pure_list_count(app.lists[KEEP]));
+ pure_app_stop(&app);
+ /* Back in a, keep is stored again: what the store has of it is what it held when it stopped being stored. */
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(app.values[N].num == 5 && pure_list_count(app.lists[KEEP]) == 4 && app.values[T_].num == 0 &&
+ !pure_list_count(app.lists[LOG]) && pure_draft_size(app.texts[NOTE_]) == 1);
+ pure_app_stop(&app);
+ remove_store(path);
+}
+
static void stored_states(void) {
static const char *const list_words[] = {"a", "\xea\xb0\x80", "", "\n", "\xf0\x9f\x98\x80"};
static const char *const text_words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""};
@@ -2057,6 +2143,7 @@ static void stored_states(void) {
stored_frames();
stored_messages();
stored_migration();
+ transient_states();
}
/* ---- Deferred saving ---------------------------------------------------- */