feat: FileIoOps fault-injection seam; close FileActionLog/FileOfflineQueue I/O gaps - #104
Merged
Merged
Conversation
added 3 commits
August 15, 2026 22:52
… gaps
Adds morph::core::FileIoOps (include/morph/core/file_io_ops.hpp): the raw
fwrite/fflush/fsync/fopen/file-open/resize_file calls FileActionLog and
FileOfflineQueue both make, as an injectable strategy. Every member
defaults to the real syscall it stands in for, so a default-constructed
FileIoOps -- and every existing call site that never passes one -- is
byte-for-byte what was called directly before this seam existed. No
behavior change for any real caller.
Wires it through FileActionLog (constructor takes an optional second
FileIoOps parameter, default {}) and closes every branch
#97 requested this exact seam for:
- append()'s short-fwrite() throw
- flush()'s failing fflush()/fsync() throws (and forgetting the
unflushed idempotencyKeys on either)
- rotate()'s pre-rotation failing fflush()/fsync() (nothing closed or
renamed) and its failing reopen after a successful rename (leaves
the log closed; requireOpen()'s throwing arm and the destructor's
null check are both reachable from this one scenario)
- repairTornTail()'s unreadable-path and failing resize_file() arms
8 new fault-injection tests in test_action_log_phase2.cpp, each
overriding exactly one FileIoOps member and leaving the rest at their
real defaults. Full morph_tests suite (1044 cases / 10034 assertions)
passes; the whole-file behavior around each injected failure was
verified to still work normally (a retry after clearing the injected
failure actually succeeds and is durably recorded).
docs/spec/journal/journal.md updated for the new constructor
signature, per CLAUDE.md.
…O gaps
Same shared seam FileActionLog now uses (morph::core::FileIoOps) --
FileOfflineQueue has the identical class of gap: several branches only
ran when a real OS-level file-I/O call failed partway through an
otherwise-successful operation. Constructor takes an optional second
FileIoOps parameter (default {}), and every fwrite/fflush/fsync/fopen
call site (writeLine/syncFile, and compact()'s own temp-file write)
now routes through it, syncFile changed from static to an instance
method since it reads _io.
6 new fault-injection tests in test_file_offline_queue.cpp: the
constructor's own append-mode fopen() failing, enqueue()'s short
fwrite()/failing fflush()/failing fsync(), and construction-time
compact()'s own short fwrite()/failing fflush(). Full morph_tests
suite (1044 cases / 10034 assertions) passes.
docs/spec/offline/offline.md updated for the new constructor
signature, per CLAUDE.md.
Was missing from the previous commits -- the Header <-> spec sync gate correctly caught it (include/morph/core/file_io_ops.hpp is new, but no docs/spec/core/** file changed alongside it). One file per public type, per CLAUDE.md.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Closes #97 for real, and the identical gap in
FileOfflineQueue(found during the coverage sweep -- the same class ofbranch was flagged there too, blocked on the same missing seam).
What this adds
morph::core::FileIoOps(include/morph/core/file_io_ops.hpp): the rawfwrite/fflush/fsync/fopen/file-open/resize_filecalls bothFileActionLogandFileOfflineQueuemake, as an injectable strategy.Every member defaults to the real syscall it stands in for, so a
default-constructed
FileIoOps-- and every existing call site thatnever passes one -- is byte-for-byte what was called directly before
this seam existed. No behavior change for any real caller.
Both classes' constructors now take an optional second
FileIoOpsparameter (default
{}), and every raw I/O call routes through it.Branches closed
FileActionLog(morph#97's own list):append()'s short-fwrite()throwflush()'s failingfflush()/fsync()(and forgetting theunflushed idempotencyKeys on either)
rotate()'s pre-rotation failingfflush()/fsync()(nothingclosed or renamed) and its failing reopen after a successful rename
-- the one scenario morph#97 called out as needing the most
real-world contortion to reach:
requireOpen()'s throwing arm andthe destructor's null check are both reachable from this single
scenario, and now covered by one test
repairTornTail()'s unreadable-path and failing-resize_file()armsFileOfflineQueue(same class of gap, found this session):fopen()failingenqueue()'s shortfwrite(), failingfflush(), failingfsync()compact()'s own shortfwrite()and failingfflush()14 new fault-injection tests total (8 + 6), each overriding exactly one
FileIoOpsmember and leaving the rest at their real defaults -- thesurrounding I/O still touches the real filesystem normally, and a retry
after clearing the injected failure was verified to actually succeed
and be durably recorded.
Verification
Full
morph_testssuite: 1044 test cases, 10034 assertions, allpassing -- both before this change (proving no regression for
existing callers) and after (with the new tests included).
docs/spec/journal/journal.mdanddocs/spec/offline/offline.mdupdated for both constructors' new signature, per CLAUDE.md.