Last active
August 5, 2022 11:08
-
-
Save rgarner/7c3a901737c6829dbe29d9b5e97e3942 to your computer and use it in GitHub Desktop.
Print the small constellation of objects in your integration test and how they relate. `include Diagram`, then `save_and_open_diagram`
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
# Print the small constellation of objects in your integration test and how they relate. | |
# Requires Graphviz. Optimised for Mac. YMMV. | |
module Diagram | |
def save_and_open_diagram(**args) | |
DotGenerator.new(**args).open | |
end | |
# Collect RSpec `let`s for a given example and all her parents | |
class RSpecLets | |
def initialize(example) | |
@example = example | |
end | |
def collect | |
_collect(@example.class, []) | |
end | |
private | |
def _collect(klass, lets) | |
lets.tap do | |
next if klass.to_s == 'RSpec::ExampleGroups' | |
lets.concat(klass::LetDefinitions.instance_methods(false)) | |
parent_class = klass.to_s.deconstantize.constantize | |
_collect(parent_class, lets) | |
end | |
end | |
end | |
# Get a visual handle on what small constellations of objects we're creating | |
# in specs | |
class DotGenerator | |
def initialize(label: 'g', output_filename: 'tmp/models.svg', attrs: false) | |
@label = label | |
@output_filename = output_filename | |
@options = { attrs: attrs } | |
example = binding.of_caller(1).eval('self') | |
collect_lets(example) | |
end | |
def dot | |
renderer = ERB.new(template, trim_mode: '-') | |
renderer.result(self.binding) | |
end | |
def open | |
FileUtils.rm(@output_filename, force: true) | |
IO.popen("dot -Tsvg -o #{@output_filename}", 'w') do |pipe| | |
pipe.puts(dot).tap {|d| File.open('tmp/models.dot', 'w+') { |f| f.write(dot) }} | |
end | |
warn "Written to #{@output_filename}" | |
open_command = `which open`.chomp | |
`#{open_command} #{@output_filename}` if open_command.present? | |
end | |
private | |
def collect_lets(example) | |
@lets_by_value = RSpecLets.new(example).collect.each_with_object({}) do |sym, lets_by_value| | |
value = example.send(sym) | |
lets_by_value[value] = sym if value.is_a?(ApplicationRecord) | |
end | |
end | |
def models(only_with_records: true) | |
@models ||= begin | |
Rails.application.eager_load! | |
ApplicationRecord.descendants.reject { |c| c.to_s == 'Schema' || (only_with_records && c.count == 0) } | |
end | |
end | |
def instances | |
@instances ||= models.each_with_object([]) do |klass, array| | |
array << klass.all | |
end.flatten | |
end | |
class Relationship | |
attr_accessor :source, :destination | |
def initialize(source, destination) | |
self.source = source | |
self.destination = destination | |
end | |
def equals(other) | |
source == other.source && destination == other.destination | |
end | |
end | |
def instance_name(instance) | |
"#{instance.model_name}##{instance.id}" | |
end | |
# A Set of relationships to other identified entities | |
def relationships | |
@relationships ||= instances.each_with_object(Set.new) do |instance, set| | |
add_relationships(instance, set) | |
end | |
end | |
def add_relationships(instance, set) | |
reflect_associations(instance).each do |association| | |
case association | |
when ActiveRecord::Reflection::HasManyReflection | |
records = instance.send(association.name) | |
records.each do |record| | |
set.add(Relationship.new(instance_name(instance), instance_name(record))) | |
end | |
when ActiveRecord::Reflection::HasOneReflection | |
one = instance.send(association.name) | |
set.add(Relationship.new(instance_name(instance), instance_name(one))) if one.present? | |
end | |
end | |
end | |
def reflect_associations(instance) | |
( | |
instance.class.reflect_on_all_associations(:has_many).reject { |a| a.name == :schemas } + | |
instance.class.reflect_on_all_associations(:has_one) | |
).flatten | |
end | |
def escape_hash(v) | |
v.each_with_object([]) do |(key, value), array| | |
array << "#{key}: #{value.nil? ? 'nil' : value}" | |
end.join(', ') | |
end | |
def attributes(instance) | |
instance.attributes.to_h.transform_values { |v| v.is_a?(Hash) ? escape_hash(v) : v } | |
end | |
def template | |
<<~ERB | |
digraph "<%= @label %>" { | |
rankdir = "LR"; | |
ranksep = "0.5"; | |
nodesep = "0.4"; | |
pad = "0.4,0.4"; | |
margin = "0,0"; | |
concentrate = "true"; | |
labelloc = "t"; | |
fontsize = "13"; | |
fontname = "Arial BoldMT"; | |
splines = "spline"; | |
node[ shape = "Mrecord" , fontsize = "10" , fontname = "ArialMT" , margin = "0.07,0.05" , penwidth = "1.0"]; | |
edge[ fontname = "ArialMT" , fontsize = "7" , dir = "both" , arrowsize = "0.9" , penwidth = "1.0" , labelangle = "32" , labeldistance = "1.8"]; | |
rankdir = "TB"; | |
splines = "spline"; | |
<% instances.each do |instance| %> | |
"<%= instance_name(instance) %>" [ | |
label=< | |
<table border="0" cellborder="0"> | |
<% if @lets_by_value[instance] %> | |
<tr><td><font face="Monaco" point-size="8">let(:<%= @lets_by_value[instance] %>)</font></td></tr> | |
<% end %> | |
<tr><td><%= instance_name(instance) %></td></tr> | |
</table> | |
<% if @options[:attrs] %> | |
| | |
<table border="0" cellborder="0"> | |
<% attributes(instance).each_pair do |attr, value| %> | |
<tr> | |
<td align="left" width="200" port="<%= attr %>"> | |
<%= attr %> | |
</td> | |
<td align="left"> | |
<font color="grey60"><%= value.inspect %></font> | |
</td> | |
</tr> | |
<% end %> | |
</table> | |
<% end %> | |
> | |
]; | |
<% end %> | |
<% relationships.each do |relationship| %> | |
"<%= relationship.source %>" -> "<%= relationship.destination %>" [arrowhead = "none", arrowtail = "normal", weight = "6"]; | |
<% end %> | |
} | |
ERB | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment