Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created October 23, 2019 02:09
Show Gist options
  • Save mcmire/10f5e94bcfab7c08eef1ccba81e2e245 to your computer and use it in GitHub Desktop.
Save mcmire/10f5e94bcfab7c08eef1ccba81e2e245 to your computer and use it in GitHub Desktop.
Produces a tree of ActiveRecord + Fabrication calls
# This file allows to you to debug use of fabricators in tests by producing a hierarchical tree of
# objects created via ActiveRecord and Fabrication.
#
# Place this at spec/support/active_record_persistence_instrumenter.rb.
#
module PersistenceInstrumenter
class Tree
def initialize
@stack = []
@children = []
end
def add_child(object, type:, level:, **rest)
Node.add(object, type: type, level: level, **rest).tap do |new_node|
children << new_node
end
end
def instrumenting(node_type, object, **rest)
unless_check = rest.delete(:unless)
parent = stack.last || self
if unless_check && unless_check.call(stack.last)
yield
else
node = parent.add_child(
object,
type: node_type,
level: stack.size,
**rest
)
stack << node
value = yield node
stack.pop
value
end
end
def to_s
children.map(&:to_s).join("\n")
end
private
attr_reader :stack, :children
class Node
STRATEGIES = {}
def self.add(object, type:, level:, **rest)
STRATEGIES.fetch(type)
.new(object, level: level, index: last_index, **rest)
.tap { increment_index }
end
def self.register_strategy(name, klass)
STRATEGIES[name] = klass
end
def self.last_index
@last_index ||= 0
end
def self.increment_index
@last_index += 1
end
attr_reader :object
def initialize(object, level:, index:)
@object = object
@level = level
@index = index
@children = []
end
def add_child(parent, type:, level:, **rest)
self.class.add(parent, type: type, level: level, **rest)
.tap { |new_child| children << new_child }
end
def to_s
message = indent(header)
if children.any?
message << " {\n"
message << children.map { |child| "#{child.to_s}\n" }.join
message << indent("}")
end
message
end
private
attr_reader :type, :level, :index, :children
def indent(string)
indentation + string
end
def indentation
" " * level
end
end
class ActiveRecordNode < Node
def initialize(*)
super
@already_persisted = object.persisted?
end
protected
def header
header =
"#{object.class.name.colorize(:blue)}(" +
"id: " +
"#{object.id}".colorize(:magenta) +
")"
if already_persisted?
header << " [already persisted]".colorize(:light_black)
end
header
end
private
def already_persisted?
@already_persisted
end
Node.register_strategy(:active_record, self)
end
class FabricatorNode < Node
attr_reader :mode
attr_accessor :record
def initialize(*args, mode: :fabricate, **rest)
super(*args, **rest)
@mode = mode
end
protected
def header
header =
"Fabricate.#{mode}(" +
":#{object.name}".colorize(:yellow) +
", klass: " +
"#{object.klass}".colorize(:blue)
if record
header << ", id: " + "#{record.id}".colorize(:magenta)
end
header << ")"
end
private
Node.register_strategy(:fabricator, self)
end
end
module ActiveRecordExtensions
def create_or_update(*)
if PersistenceInstrumenter.enabled?
PersistenceInstrumenter.tree.instrumenting(:active_record, self) { super }
else
super
end
end
end
module FabricationExtensions
def build(overrides={}, &block)
if PersistenceInstrumenter.enabled?
PersistenceInstrumenter.tree.instrumenting(
:fabricator,
self,
mode: :build,
unless: -> (parent) {
parent &&
parent.respond_to?(:mode) &&
parent.mode == :fabricate &&
parent.object.name == name &&
parent.object.klass == klass
}
) do
super
end
else
super
end
end
def fabricate(overrides={}, &block)
if PersistenceInstrumenter.enabled?
mode =
if Fabrication.manager.build_stack.any?
:build
else
:create
end
PersistenceInstrumenter.tree.instrumenting(
:fabricator,
self,
mode: mode
) do |node|
super.tap do |object|
if object.respond_to?(:persisted?) && object.persisted?
node.record = object
end
end
end
else
super
end
end
end
class << self
attr_accessor :tree
end
def self.reporting
@enabled = true
yield
@enabled = false
write_report
end
def self.write_report
file = Rails.root.join("tmp/persistence_instrumenter/report.txt")
file.parent.mkpath
File.write(file, tree.to_s)
@last_file_written = file
end
def self.report_file_written
if @last_file_written
puts(
"Report written to " +
@last_file_written.relative_path_from(Rails.root).to_s.colorize(:blue)
)
end
end
def self.enabled?
@enabled
end
@enabled = false
@tree = Tree.new
end
ActiveRecord::Base.class_eval do
prepend PersistenceInstrumenter::ActiveRecordExtensions
end
Fabrication::Schematic::Definition.class_eval do
prepend PersistenceInstrumenter::FabricationExtensions
end
RSpec.configure do |config|
config.around(:example, instrument_active_record_persistence: true) do |example|
PersistenceInstrumenter.reporting(&example)
end
config.after(:suite) do
PersistenceInstrumenter.report_file_written
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment