Created
October 23, 2019 02:09
-
-
Save mcmire/10f5e94bcfab7c08eef1ccba81e2e245 to your computer and use it in GitHub Desktop.
Produces a tree of ActiveRecord + Fabrication calls
This file contains 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
# 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