Stop opening a deck from rewriting it - #25
Merged
Merged
Conversation
Studying any deck in examples/ straight from a clone left it modified in git status without a single card having been answered. That looks like the app corrupting its own sample data, and it is an easy way to get surprise diffs. Two causes, both of which had to go. Cards were given an id when the deck was loaded. An id is what a log event names a card by, so a card needs one at the moment something is recorded against it, and a deck that is only read has nothing recorded against it. Deck::ensure_id mints one there instead. The id still exists before the event naming it is written, which is the invariant that matters. Saving then expanded every row to its full width, so a hand-written question,answer,tags deck came back as question,answer,tags,0,0,1,,,id. card_to_csv now writes a card only as far as the last column it actually uses. That rule already existed for the audio and image columns for exactly this reason; it now covers the whole row instead of the last two, and the defaults it trims against are the ones card_from_csv fills in for a column that is absent, so the round trip is unchanged. A save that would reproduce the file byte for byte is skipped outright. Every call site saves unconditionally, which is what makes an interrupted session cost nothing; it should not also mean that reading a deck counts as writing it. Six golden transcripts change, all of them cases that browse, tag, list or quit without answering, and all of them in the same direction: the deck fixture stays as it was written. Cases that answer a card are untouched, and still show the deck's id matching the log's card_id.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Studying any deck in
examples/straight from a clone left it modified ingit statuswithout a single card having been answered:All twenty rows rewritten. That looks like the app corrupting its own sample data, it is an easy way to get surprise diffs, and it is the first thing a curious visitor does.
Two causes, both of which had to go
Cards were given an
idwhen the deck was loaded. An id is what a log event names a card by, so a card needs one at the moment something is about to be recorded against it — and a deck that is only read has nothing recorded against it.Deck::ensure_idmints one there instead, fromlog_answerandlog_undo.The invariant that matters is unchanged: the id still exists before the event naming it is written. Golden cases that answer a card still show the deck's
idmatching the log'scard_id.Saving then expanded every row to its full width, so a hand-written
question,answer,tagsdeck came back asquestion,answer,tags,0,0,1,,,<id>. Removing the minting alone would not have fixed anything — the file still differed.card_to_csvnow writes a card only as far as the last column it actually uses. That rule already existed for theaudioandimagecolumns for exactly this reason; it now covers the whole row instead of the last two. The values it trims against are the onescard_from_csvfills in for a column that is absent, so the round trip is unchanged — the two lists sit next to each other in the function with a comment saying they have to stay in step.And a save that would change nothing is skipped
Every call site saves unconditionally — after every answer, after every edit, and on the way out. That is deliberate and is what makes an interrupted session cost nothing. It should not also mean that reading a deck counts as writing it, so
Deck::savecompares against what it read and returns success without touching the file.A deck file that does not exist yet is still created, even when the deck is empty and serialises to nothing: the "have I seen the file" flag and an empty string are not the same question, and there is a test for exactly that.
Verification
All eleven shipped decks confirmed byte-identical by md5 after being opened and closed. And from a fresh clone of this branch:
make checkpasses and studying an example leaves the tree clean.Six golden transcripts change, all cases that browse, tag, list, or quit without answering, all in the same direction — the deck fixture stays as it was written. Cases that answer a card are untouched.
New coverage:
test_save_leaves_an_unchanged_deck_alone, and the id test now asserts the deferred behaviour rather than the old load-time minting.Compatibility
Nothing about what a deck can contain has changed. Every deck an earlier version wrote still loads, and still saves identically once a card in it has been reviewed — a reviewed card has a non-default box, dates and id, so it writes the full row exactly as before. Only never-reviewed, id-less cards take the short form, and those are precisely the ones that came from a short-form file.
Checks
make checkon gcc and clang, optimised and sanitised (-fsanitize=address,undefined -fno-sanitize-recover=undefined), theenv -iclean room, and a fresh clone. 1201 unit checks, 47 golden cases.