Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/fixture_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class DuplicateFixtureError < Error; end
class DuplicateNameError < Error; end
class CacheMissingError < Error; end
class PregenerationError < Error; end
class FixtureDefinitionNotFound < Error; end
class ExposedRecordNotFound < Error; end

autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
Expand Down
2 changes: 1 addition & 1 deletion lib/fixture_kit/fixture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Fixture
attr_reader :name, :block

def initialize(name, &block)
@name = name.to_sym
@name = name
@block = block
end

Expand Down
22 changes: 11 additions & 11 deletions lib/fixture_kit/fixture_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class << self

def clear_memory_cache(fixture_name = nil)
if fixture_name
@memory_cache.delete(fixture_name.to_s)
@memory_cache.delete(fixture_name)
else
@memory_cache.clear
end
Expand All @@ -34,26 +34,26 @@ def clear(fixture_name = nil)
end
end

# Pre-generate caches for all fixtures.
# Generate caches for all fixtures.
# Each fixture is generated in a transaction that rolls back, so no data persists.
def pregenerate_all
fixture_path = FixtureKit.configuration.fixture_path
def generate_all
FixtureRegistry.load_definitions
FixtureRegistry.fixtures.each { |fixture| generate(fixture.name) }
end

# First, load all fixture files to register them
FixtureRegistry.load_definitions(fixture_path)
def generate(fixture_name)
clear(fixture_name)

# Then iterate over the registry and generate caches
FixtureRegistry.all_names.each do |name|
runner = FixtureRunner.new(name)
runner.generate_cache_only
FixtureKit.configuration.generator.run do
FixtureRunner.run(fixture_name, force: true)
end
end
end

attr_reader :records, :exposed

def initialize(fixture_name)
@fixture_name = fixture_name.to_s
@fixture_name = fixture_name
@records = {}
@exposed = {}
end
Expand Down
54 changes: 29 additions & 25 deletions lib/fixture_kit/fixture_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,55 @@
module FixtureKit
module FixtureRegistry
class << self
def register(fixture)
fixtures[fixture.name.to_s] = fixture
def fetch(name)
fixture = find(name)
return fixture if fixture

file_path = fixture_file_path(name)
unless File.file?(file_path)
raise FixtureKit::FixtureDefinitionNotFound,
"Could not find fixture definition file for '#{name}' at '#{file_path}'"
end

load file_path
find(name)
end

def find(name)
fixtures[name.to_s]
registry[name]
end

def all_names
fixtures.keys
def fixtures
registry.values
end

def register(fixture)
registry[fixture.name] = fixture
end

# Load all fixture definition files from the given path.
# Load all fixture definition files.
# Uses `load` instead of `require` to ensure fixtures are registered
# even if the files were previously required (e.g., after a reset).
def load_definitions(fixture_path)
def load_definitions
fixture_path = FixtureKit.configuration.fixture_path
Dir.glob(File.join(fixture_path, "**/*.rb")).each do |file|
load file
end
end

def reset
@fixtures = nil
@registry = nil
end

# Load a fixture's records into the database and return a FixtureSet.
# Uses cached INSERT statements if available, otherwise executes fixture and caches.
def load_fixture(name)
name = name.to_s

# Load the file on-demand if fixture not yet registered
unless find(name)
fixture_path = FixtureKit.configuration.fixture_path
file_path = File.expand_path(File.join(fixture_path, "#{name}.rb"))
load file_path
end
private

runner = FixtureRunner.new(name)
runner.run
def fixture_file_path(name)
fixture_path = FixtureKit.configuration.fixture_path
File.expand_path(File.join(fixture_path, "#{name}.rb"))
end

private

def fixtures
@fixtures ||= {}
def registry
@registry ||= {}
end
end
end
Expand Down
31 changes: 10 additions & 21 deletions lib/fixture_kit/fixture_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

module FixtureKit
class FixtureRunner
def self.run(fixture_name, force: false)
new(fixture_name).run(force: force)
end

def initialize(fixture_name)
@fixture_name = fixture_name.to_sym
@fixture_name = fixture_name
@cache = FixtureCache.new(@fixture_name)
end

def run
if @cache.exists?
def run(force: false)
if force
execute_and_cache
elsif @cache.exists?
execute_from_cache
elsif FixtureKit.configuration.autogenerate
execute_and_cache
Expand All @@ -26,27 +32,10 @@ def run
end
end

# Generate cache only (used for pregeneration in before(:suite))
# Wraps execution in the configured generator lifecycle.
# The default generator uses ActiveRecord::TestFixtures transactions.
# Entry points (like `fixture_kit/rspec`) can install richer generators.
# Always regenerates the cache, even if one exists
def generate_cache_only
# Clear any existing cache for this fixture
FixtureCache.clear(@fixture_name.to_s)

FixtureKit.configuration.generator.run do
execute_and_cache
end

true
end

private

def execute_and_cache
fixture = FixtureRegistry.find(@fixture_name)
raise ArgumentError, "Fixture '#{@fixture_name}' not found" unless fixture
fixture = FixtureRegistry.fetch(@fixture_name)

# Start capturing SQL
capture = SqlCapture.new
Expand Down
13 changes: 12 additions & 1 deletion lib/fixture_kit/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,22 @@ def self.configure!(config)
preserve_cache = ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
FixtureCache.clear unless preserve_cache
else
FixtureCache.pregenerate_all
fixture_names_for_loaded_examples.each do |fixture_name|
FixtureCache.generate(fixture_name)
end
end
end
end
end

def self.fixture_names_for_loaded_examples
::RSpec.world.filtered_examples.each_value.with_object(Set.new) do |examples, names|
examples.each do |example|
declaration = example.metadata[DECLARATION_METADATA_KEY]
names << declaration.name if declaration
end
end.to_a
end
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/fixture_kit/rspec/declaration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ class Declaration
attr_reader :name

def initialize(name)
@name = name.to_s
@name = name
end

def fixture_set
FixtureKit::FixtureRegistry.load_fixture(name)
FixtureKit::FixtureRunner.run(name)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/support/dummy_rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# Helper to load fixtures in tests (wraps internal API)
def load_fixture(name)
FixtureKit::FixtureRegistry.load_fixture(name)
FixtureKit::FixtureRunner.run(name)
end

# Helper to clear fixture cache in tests
Expand Down
45 changes: 35 additions & 10 deletions spec/unit/fixture_cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,6 @@
expect(described_class.memory_cache).to be_empty
end

it "handles symbol fixture names" do
described_class.clear_memory_cache(:fixture_a)

expect(described_class.memory_cache.key?("fixture_a")).to be(false)
end
end

describe ".clear" do
Expand Down Expand Up @@ -244,7 +239,7 @@
end
end

describe ".pregenerate_all" do
describe ".generate_all" do
before do
described_class.clear
end
Expand All @@ -256,7 +251,7 @@
expect(File.exist?(project_cache)).to be(false)
expect(File.exist?(teams_cache)).to be(false)

described_class.pregenerate_all
described_class.generate_all

expect(File.exist?(project_cache)).to be(true)
expect(File.exist?(teams_cache)).to be(true)
Expand All @@ -268,7 +263,7 @@
expect(ActivityLog.count).to eq(0)
expect(TimeEntry.count).to eq(0)

described_class.pregenerate_all
described_class.generate_all

# Database should still be empty (transactions rolled back)
expect(User.count).to eq(0)
Expand All @@ -282,20 +277,50 @@

it "regenerates caches even if they already exist" do
# Generate cache
described_class.pregenerate_all
described_class.generate_all

project_cache = File.join(cache_path, "project_management.json")

# Corrupt the cache file with invalid content
File.write(project_cache, '{"records": {}, "exposed": {}}')

# Pregenerate again - should overwrite with valid content
described_class.pregenerate_all
described_class.generate_all

# Cache should have actual records now
cache_data = JSON.parse(File.read(project_cache))
expect(cache_data["records"]).to have_key("User")
expect(cache_data["exposed"]).to have_key("alice")
end
end

describe ".generate" do
before do
described_class.clear
end

it "uses the configured generator lifecycle" do
generator = class_double("CustomGenerator")
previous_generator = FixtureKit.configuration.generator
FixtureKit.configuration.generator = generator

expect(generator).to receive(:run).and_yield
expect(described_class).to receive(:clear).with("project_management")
expect(FixtureKit::FixtureRunner).to receive(:run).with("project_management", force: true)

described_class.generate("project_management")
ensure
FixtureKit.configuration.generator = previous_generator
end

it "can generate cache for a selected fixture only" do
project_cache = File.join(cache_path, "project_management.json")
teams_cache = File.join(cache_path, "teams/basic.json")

described_class.generate("project_management")

expect(File.exist?(project_cache)).to be(true)
expect(File.exist?(teams_cache)).to be(false)
end
end
end
24 changes: 24 additions & 0 deletions spec/unit/fixture_registry_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe FixtureKit::FixtureRegistry do
describe ".fetch" do
let(:fixture_path) { Rails.root.join("spec/fixture_kit").to_s }

before do
FixtureKit.configuration.fixture_path = fixture_path
described_class.reset
end

it "raises a custom error when the fixture file does not exist" do
expect do
described_class.fetch("does/not_exist")
end.to raise_error(
FixtureKit::FixtureDefinitionNotFound,
/Could not find fixture definition file for 'does\/not_exist'/
)
end
end

end
19 changes: 14 additions & 5 deletions spec/unit/fixture_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@
require "spec_helper"

RSpec.describe FixtureKit::FixtureRunner do
describe "#generate_cache_only" do
it "uses the configured generator" do
describe "#run" do
it "does not use the configured generator when force is true" do
generator = class_double("CustomGenerator")
runner = described_class.new("project_management")

previous_generator = FixtureKit.configuration.generator
FixtureKit.configuration.generator = generator

expect(FixtureKit::FixtureCache).to receive(:clear).with("project_management")
expect(generator).to receive(:run).and_yield
expect(generator).not_to receive(:run)
expect(runner).to receive(:execute_and_cache)

expect(runner.generate_cache_only).to be(true)
runner.run(force: true)
ensure
FixtureKit.configuration.generator = previous_generator
end

it "bypasses cache reads when force is true" do
runner = described_class.new("project_management")

allow(runner.instance_variable_get(:@cache)).to receive(:exists?).and_return(true)
expect(runner).not_to receive(:execute_from_cache)
expect(runner).to receive(:execute_and_cache)

runner.run(force: true)
end
end
end
Loading