From afc57cf5d58c8e2931479c2e39aca5069d46d5f8 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Wed, 3 Jun 2026 15:14:04 +0200 Subject: [PATCH] Raise an actionable error when a cache predates a coder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MemoryCache#data_for` did a bare `data.fetch(coder_class)`, so when the on-disk cache had no entry for a registered coder it surfaced as an opaque `KeyError: key not found: FixtureKit::SomeCoder`. This happens when a coder is registered in the consuming process that was not registered when the cache was written — most commonly under `FIXTURE_KIT_PRESERVE_CACHE=1`, where a cache from a previous run is read by a process that has since added a coder. The lookup runs from both `Cache#load` (mount) and `Cache#evaluate` (parent data), so either path could hit it. Replace the bare fetch with a fetch block that raises `FixtureKit::MissingCoderDataError`, naming the coder and pointing at cache regeneration — same fail-loud, stale-cache-hint style as the foreign-key check in `ActiveRecordCoder#mount`. --- docs/reference.md | 1 + lib/fixture_kit.rb | 1 + lib/fixture_kit/memory_cache.rb | 8 +++++++- spec/unit/memory_cache_spec.rb | 24 ++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/reference.md b/docs/reference.md index 4a78518..366686d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -322,6 +322,7 @@ Public error classes: - `FixtureKit::FixtureDefinitionNotFound` - `FixtureKit::RunnerAlreadyStartedError` - `FixtureKit::CircularFixtureInheritance` +- `FixtureKit::MissingCoderDataError` ## Requirements diff --git a/lib/fixture_kit.rb b/lib/fixture_kit.rb index 7caf094..e6458f7 100644 --- a/lib/fixture_kit.rb +++ b/lib/fixture_kit.rb @@ -9,6 +9,7 @@ class CacheMissingError < Error; end class FixtureDefinitionNotFound < Error; end class RunnerAlreadyStartedError < Error; end class CircularFixtureInheritance < Error; end + class MissingCoderDataError < Error; end autoload :VERSION, File.expand_path("fixture_kit/version", __dir__) autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__) diff --git a/lib/fixture_kit/memory_cache.rb b/lib/fixture_kit/memory_cache.rb index dd9aa1c..61f166e 100644 --- a/lib/fixture_kit/memory_cache.rb +++ b/lib/fixture_kit/memory_cache.rb @@ -11,7 +11,13 @@ def initialize(data:, exposed:) end def data_for(coder_class) - data.fetch(coder_class) + data.fetch(coder_class) do + raise MissingCoderDataError, + "The fixture cache has no data for #{coder_class}. This usually means " \ + "the cache was written before #{coder_class} was registered as a coder. " \ + "Regenerate the fixture cache so it includes this coder (clear the cache " \ + "directory, or run without FIXTURE_KIT_PRESERVE_CACHE=1)." + end end end end diff --git a/spec/unit/memory_cache_spec.rb b/spec/unit/memory_cache_spec.rb index 3fb916a..4a1a4b7 100644 --- a/spec/unit/memory_cache_spec.rb +++ b/spec/unit/memory_cache_spec.rb @@ -20,4 +20,28 @@ expect(cache).to be_frozen end end + + describe "#data_for" do + it "returns the data stored for a coder class" do + cache = described_class.new( + data: { FixtureKit::ActiveRecordCoder => { "User" => "SQL" } }, + exposed: {} + ) + + expect(cache.data_for(FixtureKit::ActiveRecordCoder)).to eq({ "User" => "SQL" }) + end + + it "raises an actionable error when the cache predates a coder" do + # A cache written before this coder was registered has no entry for it. + # Rather than an opaque KeyError, callers should learn the cache is stale + # and how to fix it — naming the coder and pointing to regeneration. + cache = described_class.new(data: {}, exposed: {}) + + expect { cache.data_for(FixtureKit::ActiveRecordCoder) } + .to raise_error(FixtureKit::MissingCoderDataError) do |error| + expect(error.message).to include("FixtureKit::ActiveRecordCoder") + expect(error.message).to match(/regenerate/i) + end + end + end end