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
2 changes: 2 additions & 0 deletions lib/fixture_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class CircularFixtureInheritance < Error; end
autoload :Repository, File.expand_path("fixture_kit/repository", __dir__)
autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
autoload :FileCache, File.expand_path("fixture_kit/file_cache", __dir__)
autoload :MemoryCache, File.expand_path("fixture_kit/memory_cache", __dir__)
autoload :Runner, File.expand_path("fixture_kit/runner", __dir__)
autoload :Adapter, File.expand_path("fixture_kit/adapter", __dir__)
autoload :MinitestAdapter, File.expand_path("fixture_kit/adapters/minitest_adapter", __dir__)
Expand Down
58 changes: 16 additions & 42 deletions lib/fixture_kit/cache.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# frozen_string_literal: true

require "json"
require "fileutils"
require "active_support/core_ext/array/wrap"
require "active_support/inflector"

module FixtureKit
class Cache
ANONYMOUS_DIRECTORY = "_anonymous"
MemoryData = Data.define(:records, :exposed)

include ConfigurationHelper

Expand All @@ -19,7 +15,7 @@ def initialize(fixture)
end

def path
File.join(configuration.cache_path, "#{identifier}.json")
file_cache.path
end

def identifier
Expand All @@ -34,15 +30,19 @@ def identifier
end

def exists?
data || File.exist?(path)
data || file_cache.exists?
end

def clear_memory
@data = nil
end

def load
unless exists?
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
end

@data ||= load_memory_data
@data ||= file_cache.read
statements_by_connection(data.records).each do |connection, statements|
connection.disable_referential_integrity do
# execute_batch is private in current supported Rails versions.
Expand All @@ -64,17 +64,23 @@ def save
captured_models.concat(fixture.parent.cache.data.records.keys)
end

@data = MemoryData.new(
@data = MemoryCache.new(
records: generate_statements(captured_models),
exposed: build_exposed_mapping(fixture.definition.exposed)
exposed: file_cache.serialize_exposed(fixture.definition.exposed)
)
end

save_file_data
file_cache.write(data)
end

private

def file_cache
@file_cache ||= FileCache.new(
File.join(configuration.cache_path, "#{identifier}.json")
)
end

def generate_statements(models)
models.uniq.each_with_object({}) do |model, statements|
columns = model.column_names
Expand Down Expand Up @@ -104,16 +110,6 @@ def build_insert_sql(table_name, columns, rows, connection)
"INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
end

def build_exposed_mapping(exposed)
exposed.each_with_object({}) do |(name, record), hash|
if record.is_a?(Array)
hash[name] = record.map { |record| { record.class => record.id } }
else
hash[name] = { record.class => record.id }
end
end
end

def statements_by_connection(records)
deleted_tables = Set.new

Expand All @@ -129,27 +125,5 @@ def statements_by_connection(records)
grouped[connection] << sql if sql
end
end

def load_memory_data
file_data = JSON.parse(File.read(path))
records = file_data.fetch("records").transform_keys do |model_name|
ActiveSupport::Inflector.constantize(model_name)
end

exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
if value.is_a?(Array)
hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
else
hash[name.to_sym] = { ActiveSupport::Inflector.constantize(value.keys.first) => value.values.first }
end
end

MemoryData.new(records: records, exposed: exposed)
end

def save_file_data
FileUtils.mkdir_p(File.dirname(path))
File.write(path, JSON.pretty_generate(data.to_h))
end
end
end
51 changes: 51 additions & 0 deletions lib/fixture_kit/file_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require "json"
require "fileutils"
require "active_support/inflector"

module FixtureKit
class FileCache
attr_reader :path

def initialize(path)
@path = path
end

def exists?
File.exist?(path)
end

def read
file_data = JSON.parse(File.read(path))
records = file_data.fetch("records").transform_keys do |model_name|
ActiveSupport::Inflector.constantize(model_name)
end

exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
if value.is_a?(Array)
hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
else
hash[name.to_sym] = { ActiveSupport::Inflector.constantize(value.keys.first) => value.values.first }
end
end

MemoryCache.new(records: records, exposed: exposed)
end

def write(data)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, JSON.pretty_generate(data.to_h))
end

def serialize_exposed(exposed)
exposed.each_with_object({}) do |(name, record), hash|
if record.is_a?(Array)
hash[name] = record.map { |record| { record.class => record.id } }
else
hash[name] = { record.class => record.id }
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/fixture_kit/fixture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ def mount
emit(:cache_mounted) { @cache.load }
end

def finish
@cache.clear_memory if anonymous?
end

private

def anonymous?
!identifier.is_a?(String)
end

def emit(event)
cache_identifier = @cache.identifier
unless block_given?
Expand Down
17 changes: 17 additions & 0 deletions lib/fixture_kit/memory_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module FixtureKit
class MemoryCache
attr_reader :records, :exposed

def initialize(records:, exposed:)
@records = records
@exposed = exposed
freeze
end

def to_h
{ records: records, exposed: exposed }
end
end
end
2 changes: 2 additions & 0 deletions lib/fixture_kit/minitest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def run_suite(reporter, options = {})
end

super

declaration&.finish
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/fixture_kit/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def fixture(name = nil, extends: nil, &block)
prepend_before(:context) do
self.class.metadata[DECLARATION_METADATA_KEY].generate
end

append_after(:context) do
self.class.metadata[DECLARATION_METADATA_KEY].finish
end
end
end

Expand Down
106 changes: 106 additions & 0 deletions spec/unit/file_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe FixtureKit::FileCache do
let(:cache_path) { Rails.root.join("tmp/cache/fixture_kit_file_cache_test").to_s }
let(:file_path) { File.join(cache_path, "test_fixture.json") }
let(:file_cache) { described_class.new(file_path) }

before do
FileUtils.rm_rf(cache_path)
end

after do
FileUtils.rm_rf(cache_path)
end

describe "#path" do
it "returns the path provided at initialization" do
expect(file_cache.path).to eq(file_path)
end
end

describe "#exists?" do
it "returns false when the file does not exist" do
expect(file_cache.exists?).to be(false)
end

it "returns true when the file exists" do
FileUtils.mkdir_p(cache_path)
File.write(file_path, "{}")

expect(file_cache.exists?).to be(true)
end
end

describe "#write and #read" do
it "round-trips MemoryCache through JSON on disk" do
data = FixtureKit::MemoryCache.new(
records: { User => "INSERT INTO users (id, name) VALUES (1, 'Alice')" },
exposed: { alice: { User => 1 } }
)

file_cache.write(data)

expect(File.exist?(file_path)).to be(true)
result = file_cache.read
expect(result).to be_a(FixtureKit::MemoryCache)
expect(result.records).to eq({ User => "INSERT INTO users (id, name) VALUES (1, 'Alice')" })
expect(result.exposed).to eq({ alice: { User => 1 } })
end

it "round-trips MemoryCache with nil sql values" do
data = FixtureKit::MemoryCache.new(
records: { User => nil },
exposed: {}
)

file_cache.write(data)
result = file_cache.read

expect(result.records).to eq({ User => nil })
end

it "round-trips MemoryCache with array exposed values" do
data = FixtureKit::MemoryCache.new(
records: {},
exposed: { users: [{ User => 1 }, { User => 2 }] }
)

file_cache.write(data)
result = file_cache.read

expect(result.exposed).to eq({ users: [{ User => 1 }, { User => 2 }] })
end

it "creates intermediate directories" do
nested_path = File.join(cache_path, "nested", "deep", "fixture.json")
nested_file_cache = described_class.new(nested_path)
data = FixtureKit::MemoryCache.new(records: {}, exposed: {})

nested_file_cache.write(data)

expect(File.exist?(nested_path)).to be(true)
end
end

describe "#serialize_exposed" do
it "converts ActiveRecord instances to class/id pairs" do
user = User.create!(name: "Alice", email: "alice-file-cache@example.com")

result = file_cache.serialize_exposed({ alice: user })

expect(result).to eq({ alice: { User => user.id } })
end

it "converts arrays of ActiveRecord instances" do
alice = User.create!(name: "Alice", email: "alice-array@example.com")
bob = User.create!(name: "Bob", email: "bob-array@example.com")

result = file_cache.serialize_exposed({ users: [alice, bob] })

expect(result).to eq({ users: [{ User => alice.id }, { User => bob.id }] })
end
end
end
Loading