seafoam 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/rubocop.yml +10 -0
  4. data/.github/workflows/specs.yml +19 -0
  5. data/.gitignore +7 -0
  6. data/.rubocop.yml +34 -0
  7. data/.ruby-version +1 -0
  8. data/.seafoam/config +1 -0
  9. data/CODE_OF_CONDUCT.md +128 -0
  10. data/CONTRIBUTING.md +5 -0
  11. data/Gemfile +2 -0
  12. data/LICENSE.md +7 -0
  13. data/README.md +298 -0
  14. data/bin/bgv2isabelle +53 -0
  15. data/bin/bgv2json +42 -0
  16. data/bin/seafoam +24 -0
  17. data/docs/annotators.md +43 -0
  18. data/docs/bgv.md +284 -0
  19. data/docs/getting-graphs.md +47 -0
  20. data/examples/Fib.java +24 -0
  21. data/examples/MatMult.java +39 -0
  22. data/examples/fib-java.bgv +0 -0
  23. data/examples/fib-js.bgv +0 -0
  24. data/examples/fib-ruby.bgv +0 -0
  25. data/examples/fib.js +15 -0
  26. data/examples/fib.rb +15 -0
  27. data/examples/identity.bgv +0 -0
  28. data/examples/identity.rb +13 -0
  29. data/examples/java/Irreducible.j +35 -0
  30. data/examples/java/IrreducibleDecompiled.java +21 -0
  31. data/examples/java/JavaExamples.java +418 -0
  32. data/examples/java/exampleArithOperator.bgv +0 -0
  33. data/examples/java/exampleArithOperator.cfg +925 -0
  34. data/examples/java/exampleArrayAllocation.bgv +0 -0
  35. data/examples/java/exampleArrayAllocation.cfg +5268 -0
  36. data/examples/java/exampleArrayRead.bgv +0 -0
  37. data/examples/java/exampleArrayRead.cfg +2263 -0
  38. data/examples/java/exampleArrayWrite.bgv +0 -0
  39. data/examples/java/exampleArrayWrite.cfg +2315 -0
  40. data/examples/java/exampleCatch.bgv +0 -0
  41. data/examples/java/exampleCatch.cfg +4150 -0
  42. data/examples/java/exampleCompareOperator.bgv +0 -0
  43. data/examples/java/exampleCompareOperator.cfg +1109 -0
  44. data/examples/java/exampleDoubleSynchronized.bgv +0 -0
  45. data/examples/java/exampleDoubleSynchronized.cfg +26497 -0
  46. data/examples/java/exampleExactArith.bgv +0 -0
  47. data/examples/java/exampleExactArith.cfg +1888 -0
  48. data/examples/java/exampleFieldRead.bgv +0 -0
  49. data/examples/java/exampleFieldRead.cfg +1228 -0
  50. data/examples/java/exampleFieldWrite.bgv +0 -0
  51. data/examples/java/exampleFieldWrite.cfg +1102 -0
  52. data/examples/java/exampleFor.bgv +0 -0
  53. data/examples/java/exampleFor.cfg +3936 -0
  54. data/examples/java/exampleFullEscape.bgv +0 -0
  55. data/examples/java/exampleFullEscape.cfg +5893 -0
  56. data/examples/java/exampleIf.bgv +0 -0
  57. data/examples/java/exampleIf.cfg +2462 -0
  58. data/examples/java/exampleIfNeverTaken.bgv +0 -0
  59. data/examples/java/exampleIfNeverTaken.cfg +2476 -0
  60. data/examples/java/exampleInstanceOfManyImpls.bgv +0 -0
  61. data/examples/java/exampleInstanceOfManyImpls.cfg +6391 -0
  62. data/examples/java/exampleInstanceOfOneImpl.bgv +0 -0
  63. data/examples/java/exampleInstanceOfOneImpl.cfg +2604 -0
  64. data/examples/java/exampleIntSwitch.bgv +0 -0
  65. data/examples/java/exampleIntSwitch.cfg +3121 -0
  66. data/examples/java/exampleInterfaceCallManyImpls.bgv +0 -0
  67. data/examples/java/exampleInterfaceCallManyImpls.cfg +1358 -0
  68. data/examples/java/exampleInterfaceCallOneImpl.bgv +0 -0
  69. data/examples/java/exampleInterfaceCallOneImpl.cfg +3859 -0
  70. data/examples/java/exampleLocalInstanceOf.bgv +0 -0
  71. data/examples/java/exampleLocalInstanceOf.cfg +5276 -0
  72. data/examples/java/exampleLocalSynchronized.bgv +0 -0
  73. data/examples/java/exampleLocalSynchronized.cfg +1364 -0
  74. data/examples/java/exampleLocalVariables.bgv +0 -0
  75. data/examples/java/exampleLocalVariables.cfg +1195 -0
  76. data/examples/java/exampleLocalVariablesState.bgv +0 -0
  77. data/examples/java/exampleLocalVariablesState.cfg +1673 -0
  78. data/examples/java/exampleNestedWhile.bgv +0 -0
  79. data/examples/java/exampleNestedWhile.cfg +15499 -0
  80. data/examples/java/exampleNestedWhileBreak.bgv +0 -0
  81. data/examples/java/exampleNestedWhileBreak.cfg +11162 -0
  82. data/examples/java/exampleNoEscape.bgv +0 -0
  83. data/examples/java/exampleNoEscape.cfg +974 -0
  84. data/examples/java/exampleObjectAllocation.bgv +0 -0
  85. data/examples/java/exampleObjectAllocation.cfg +5287 -0
  86. data/examples/java/examplePartialEscape.bgv +0 -0
  87. data/examples/java/examplePartialEscape.cfg +7042 -0
  88. data/examples/java/examplePhi.bgv +0 -0
  89. data/examples/java/examplePhi.cfg +3227 -0
  90. data/examples/java/exampleReducible.bgv +0 -0
  91. data/examples/java/exampleReducible.cfg +5578 -0
  92. data/examples/java/exampleSimpleCall.bgv +0 -0
  93. data/examples/java/exampleSimpleCall.cfg +1435 -0
  94. data/examples/java/exampleStamp.bgv +0 -0
  95. data/examples/java/exampleStamp.cfg +913 -0
  96. data/examples/java/exampleStaticCall.bgv +0 -0
  97. data/examples/java/exampleStaticCall.cfg +1154 -0
  98. data/examples/java/exampleStringSwitch.bgv +0 -0
  99. data/examples/java/exampleStringSwitch.cfg +15377 -0
  100. data/examples/java/exampleSynchronized.bgv +0 -0
  101. data/examples/java/exampleSynchronized.cfg +26027 -0
  102. data/examples/java/exampleThrow.bgv +0 -0
  103. data/examples/java/exampleThrow.cfg +780 -0
  104. data/examples/java/exampleThrowCatch.bgv +0 -0
  105. data/examples/java/exampleThrowCatch.cfg +744 -0
  106. data/examples/java/exampleUnsafeRead.bgv +0 -0
  107. data/examples/java/exampleUnsafeRead.cfg +912 -0
  108. data/examples/java/exampleUnsafeWrite.bgv +0 -0
  109. data/examples/java/exampleUnsafeWrite.cfg +962 -0
  110. data/examples/java/exampleWhile.bgv +0 -0
  111. data/examples/java/exampleWhile.cfg +3936 -0
  112. data/examples/java/exampleWhileBreak.bgv +0 -0
  113. data/examples/java/exampleWhileBreak.cfg +5963 -0
  114. data/examples/matmult-java.bgv +0 -0
  115. data/examples/matmult-ruby.bgv +0 -0
  116. data/examples/matmult.rb +29 -0
  117. data/examples/overflow.bgv +0 -0
  118. data/examples/overflow.rb +13 -0
  119. data/lib/seafoam.rb +13 -0
  120. data/lib/seafoam/annotators.rb +54 -0
  121. data/lib/seafoam/annotators/fallback.rb +27 -0
  122. data/lib/seafoam/annotators/graal.rb +376 -0
  123. data/lib/seafoam/bgv/bgv_parser.rb +602 -0
  124. data/lib/seafoam/binary/binary_reader.rb +21 -0
  125. data/lib/seafoam/binary/io_binary_reader.rb +88 -0
  126. data/lib/seafoam/colors.rb +18 -0
  127. data/lib/seafoam/commands.rb +447 -0
  128. data/lib/seafoam/config.rb +34 -0
  129. data/lib/seafoam/graph.rb +91 -0
  130. data/lib/seafoam/graphviz_writer.rb +213 -0
  131. data/lib/seafoam/spotlight.rb +28 -0
  132. data/lib/seafoam/version.rb +5 -0
  133. data/seafoam.gemspec +20 -0
  134. data/spec/seafoam/annotators/fallback_spec.rb +69 -0
  135. data/spec/seafoam/annotators/graal_spec.rb +96 -0
  136. data/spec/seafoam/annotators_spec.rb +61 -0
  137. data/spec/seafoam/bgv/bgv_parser_spec.rb +144 -0
  138. data/spec/seafoam/bgv/fixtures/not.bgv +1 -0
  139. data/spec/seafoam/bgv/fixtures/unsupported.bgv +1 -0
  140. data/spec/seafoam/binary/io_binary_reader_spec.rb +176 -0
  141. data/spec/seafoam/command_spec.rb +252 -0
  142. data/spec/seafoam/graph_spec.rb +172 -0
  143. data/spec/seafoam/graphviz_writer_spec.rb +63 -0
  144. data/spec/seafoam/spec_helpers.rb +30 -0
  145. data/spec/seafoam/spotlight_spec.rb +38 -0
  146. data/tools/render-all +36 -0
  147. metadata +238 -0
@@ -0,0 +1,34 @@
1
+ module Seafoam
2
+ # Finds and loads configuration.
3
+ class Config
4
+ def initialize
5
+ @dot_dir = find_dot_dir
6
+ end
7
+
8
+ # Load the configuration.
9
+ def load_config
10
+ config_file = File.expand_path('config', @dot_dir)
11
+ if File.exist?(config_file)
12
+ puts "loading config #{config_file}" if $DEBUG
13
+ load config_file
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ # Walk up the directory chain from the current directory to root, looking
20
+ # for .seafoam.
21
+ def find_dot_dir
22
+ dir = Dir.getwd
23
+ loop do
24
+ dot_dir = File.expand_path('.seafoam', dir)
25
+ return dot_dir if Dir.exist?(dot_dir)
26
+
27
+ new_dir = File.expand_path('..', dir)
28
+ break if new_dir == dir
29
+
30
+ dir = new_dir
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ module Seafoam
2
+ # A graph, with properties, nodes, and edges. We don't encapsulate the graph
3
+ # too much - be careful.
4
+ class Graph
5
+ attr_reader :props, :nodes, :edges
6
+
7
+ def initialize(props = nil)
8
+ @props = props || {}
9
+ @nodes = {}
10
+ @edges = []
11
+ end
12
+
13
+ # Create a node.
14
+ def create_node(id, props = nil)
15
+ props ||= {}
16
+ node = Node.new(id, props)
17
+ @nodes[id] = node
18
+ node
19
+ end
20
+
21
+ # Create an edge between two nodes.
22
+ def create_edge(from, to, props = nil)
23
+ props ||= {}
24
+ edge = Edge.new(from, to, props)
25
+ @edges.push edge
26
+ from.outputs.push edge
27
+ to.inputs.push edge
28
+ edge
29
+ end
30
+ end
31
+
32
+ # A node, with properties, input edges, and output edges.
33
+ class Node
34
+ attr_reader :id, :inputs, :outputs, :props
35
+
36
+ def initialize(id, props = nil)
37
+ props ||= {}
38
+ @id = id
39
+ @inputs = []
40
+ @outputs = []
41
+ @props = props
42
+ end
43
+
44
+ # All edges - input and output.
45
+ def edges
46
+ inputs + outputs
47
+ end
48
+
49
+ # All adjacent nodes - from input and output edges.
50
+ def adjacent
51
+ (inputs.map(&:from) + outputs.map(&:to)).uniq
52
+ end
53
+
54
+ # id (label)
55
+ def id_and_label
56
+ if props[:label]
57
+ "#{id} (#{props[:label]})"
58
+ else
59
+ id.to_s
60
+ end
61
+ end
62
+
63
+ # Inspect.
64
+ def inspect
65
+ "<Node #{id}>"
66
+ end
67
+ end
68
+
69
+ # A directed edge, with a node it's from and a node it's going to, and
70
+ # properties.
71
+ class Edge
72
+ attr_reader :from, :to, :props
73
+
74
+ def initialize(from, to, props = nil)
75
+ props ||= {}
76
+ @from = from
77
+ @to = to
78
+ @props = props
79
+ end
80
+
81
+ # Both nodes - from and to.
82
+ def nodes
83
+ [@from, @to]
84
+ end
85
+
86
+ # Inspect.
87
+ def inspect
88
+ "<Edge #{from.id} -> #{to.id}>"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,213 @@
1
+ module Seafoam
2
+ # A writer from graphs to the Graphviz DOT format, including all the
3
+ # formatting.
4
+ class GraphvizWriter
5
+ def initialize(stream)
6
+ @stream = stream
7
+ end
8
+
9
+ # Write a graph.
10
+ def write_graph(graph, hidpi = false)
11
+ inline_attrs = {}
12
+ attrs = {}
13
+ attrs[:dpi] = 200 if hidpi
14
+ attrs[:bgcolor] = 'transparent'
15
+ @stream.puts 'digraph G {'
16
+ @stream.puts " graph #{write_attrs(attrs)};"
17
+ write_nodes inline_attrs, graph
18
+ write_edges inline_attrs, graph
19
+ @stream.puts '}'
20
+ end
21
+
22
+ private
23
+
24
+ # Write node declarations.
25
+ def write_nodes(inline_attrs, graph)
26
+ graph.nodes.each_value do |node|
27
+ write_node inline_attrs, node
28
+ end
29
+ end
30
+
31
+ def write_node(inline_attrs, node)
32
+ # We're going to build up a hash of Graphviz drawing attributes.
33
+ attrs = {}
34
+
35
+ # The node is hidden, and it's not going to be inlined above any
36
+ # other node.
37
+ if node.props[:hidden] && !node.props[:inlined]
38
+ # If the node has any adjacent nodes that are not hidden, and are
39
+ # shaded, then we need to declare the node but make it invisible so
40
+ # the edge appears, pointing off into space, but the node does not.
41
+ if node.adjacent.any? { |a| !a.props[:hidden] && a.props[:spotlight] == 'shaded' }
42
+ attrs[:style] = 'invis'
43
+ attrs[:label] = ''
44
+ @stream.puts " node#{node.id} #{write_attrs(attrs)};"
45
+ end
46
+ else
47
+ # This is a visible node.
48
+
49
+ # Give it a label.
50
+ if node.props[:label]
51
+ attrs[:label] = "#{node.id} #{node.props[:label]}"
52
+ else
53
+ # If we really still don't have a label, just use the ID.
54
+ attrs[:label] = node.id.to_s
55
+ end
56
+
57
+ # Basic attributes for a node.
58
+ attrs[:shape] = 'rectangle'
59
+ attrs[:fontname] = 'Arial'
60
+ attrs[:style] = 'filled'
61
+ attrs[:color] = 'black'
62
+
63
+ # Color depends on the kind of node.
64
+ back_color, fore_color = NODE_COLORS[node.props[:kind]]
65
+ attrs[:fillcolor] = back_color
66
+ attrs[:fontcolor] = fore_color
67
+
68
+ if node.props[:inlined]
69
+ # If the node is to be inlined then draw it smaller and a different
70
+ # shape.
71
+ attrs[:shape] = 'oval'
72
+ attrs[:fontsize] = '8'
73
+
74
+ # Just record these attributes for where it's used by other nodes
75
+ # so it can be drawn above them - don't actually declare a node.
76
+ inline_attrs[node.id] = attrs
77
+ else
78
+ attrs[:shape] = 'diamond' if node.props[:kind] == 'calc'
79
+
80
+ # If the node is shaded, convert the attributes to the shaded
81
+ # version.
82
+ attrs = shade(attrs) if node.props[:spotlight] == 'shaded'
83
+
84
+ # Declare the node.
85
+ @stream.puts " node#{node.id} #{write_attrs(attrs)};"
86
+ end
87
+ end
88
+ end
89
+
90
+ # Write edge declarations.
91
+
92
+ def write_edges(inline_attrs, graph)
93
+ graph.edges.each do |edge|
94
+ # Skip the edge if it's from a node that is hidden and it doesn't point
95
+ # to a shaded node.
96
+ next if edge.from.props[:hidden] && edge.to.props[:spotlight] != 'shaded'
97
+
98
+ # Skip the edge if it's to a node that is hidden and it doesn't come
99
+ # from a shaded node.
100
+ next if edge.to.props[:hidden] && edge.from.props[:spotlight] != 'shaded'
101
+
102
+ write_edge inline_attrs, edge
103
+ end
104
+ end
105
+
106
+ def write_edge(inline_attrs, edge)
107
+ # We're going to build up a hash of Graphviz drawing attributes.
108
+ attrs = {}
109
+
110
+ label = edge.props[:label]
111
+ if edge.from.props[:out_annotation]
112
+ label = "#{label} #{edge.from.props[:out_annotation]}"
113
+ end
114
+ attrs[:label] = label
115
+
116
+ # Basic edge attributes.
117
+ attrs[:fontname] = 'arial'
118
+ color = EDGE_COLORS[edge.props[:kind]]
119
+ attrs[:color] = EDGE_COLORS[edge.props[:kind]]
120
+ attrs[:fontcolor] = color
121
+
122
+ # Properties depending on the kind of edge.
123
+ case edge.props[:kind]
124
+ when 'control'
125
+ attrs[:penwidth] = 2
126
+ when 'loop'
127
+ attrs[:penwidth] = 4
128
+ when 'info'
129
+ attrs[:style] = 'dashed'
130
+ end
131
+
132
+ # Reversed edges.
133
+ attrs[:dir] = 'back' if edge.props[:reverse]
134
+
135
+ # Convert attributes to shaded if any edges involved are shaded.
136
+ attrs = shade(attrs) if edge.nodes.any? { |n| n.props[:spotlight] == 'shaded' }
137
+
138
+ # Does this edge come from an inlined node?
139
+
140
+ if edge.from.props[:inlined]
141
+ # An inlined edge is drawn as a new version of the from-node and an
142
+ # edge from that new version directly to the to-node. With only one
143
+ # user it's a short edge and the from-node is show directly above the
144
+ # to-node.
145
+
146
+ if edge.to.props[:spotlight] == 'shaded'
147
+ # Draw inlined edges to shaded nodes as invisible.
148
+ node_attrs = { label: '', style: 'invis' }
149
+ else
150
+ # Get attributes from when we went through nodes earlier.
151
+ node_attrs = inline_attrs[edge.from.id]
152
+ end
153
+
154
+ # Inlined nodes skip the arrow for simplicity.
155
+ attrs[:arrowhead] = 'none'
156
+ attrs[:fontsize] = '8'
157
+
158
+ # Declare a new node just for this user.
159
+ @stream.puts " inline#{edge.from.id}x#{edge.to.id} #{write_attrs(node_attrs)};"
160
+
161
+ # Declare the edge.
162
+ @stream.puts " inline#{edge.from.id}x#{edge.to.id} -> node#{edge.to.id} #{write_attrs(attrs)};"
163
+ else
164
+ # Declare the edge.
165
+ @stream.puts " node#{edge.from.id} -> node#{edge.to.id} #{write_attrs(attrs)};"
166
+ end
167
+ end
168
+
169
+ # Return attributes for a node or edge modified to 'shade' them in terms
170
+ # the spotlight functionality - so basically make them light grey.
171
+ def shade(attrs)
172
+ attrs = attrs.dup
173
+ attrs[:color] = ICE_STONE
174
+ attrs[:fontcolor] = ICE_STONE
175
+ attrs[:fillcolor] = DUST
176
+ attrs
177
+ end
178
+
179
+ # Write a hash of key-value attributes into the DOT format.
180
+ def write_attrs(attrs)
181
+ '[' + attrs.reject { |_k, v| v.nil? }.map { |k, v| "#{k}=#{quote(v)}" }.join(',') + ']'
182
+ end
183
+
184
+ # Quote and escape a string.
185
+ def quote(string)
186
+ string = string.to_s
187
+ string = string.gsub('\\', '\\\\')
188
+ string = string.gsub('"', '\\"')
189
+ "\"#{string}\""
190
+ end
191
+
192
+ # Color theme.
193
+
194
+ EDGE_COLORS = {
195
+ 'info' => BIG_STONE,
196
+ 'control' => AMARANTH,
197
+ 'loop' => AMARANTH,
198
+ 'data' => KEPPEL,
199
+ 'other' => BLACK
200
+ }
201
+
202
+ NODE_COLORS = {
203
+ 'info' => [DUST, BLACK],
204
+ 'input' => [WHITE_ICE, BLACK],
205
+ 'control' => [CARISSMA, BLACK],
206
+ 'effect' => [AMARANTH, WHITE],
207
+ 'virtual' => [BIG_STONE, WHITE],
208
+ 'guard' => [ORANGE, BLACK],
209
+ 'calc' => [KEPPEL, BLACK],
210
+ 'other' => [DUST, BLACK]
211
+ }
212
+ end
213
+ end
@@ -0,0 +1,28 @@
1
+ module Seafoam
2
+ # Spotlight can *light* nodes, which makes them visible, their adjacent nodes
3
+ # visible by grey, and other nodes invisible. Multiple nodes can be *lit*.
4
+ class Spotlight
5
+ def initialize(graph)
6
+ @graph = graph
7
+ end
8
+
9
+ # Mark a node as lit by the spotlight.
10
+ def light(node)
11
+ # This node is lit.
12
+ node.props[:spotlight] = 'lit'
13
+
14
+ # Adjacent nodes are shaded, if they haven't be lit themselvs.
15
+ node.adjacent.each do |adjacent|
16
+ adjacent.props[:spotlight] ||= 'shaded'
17
+ end
18
+ end
19
+
20
+ # Go through all other nodes and make them hidden, having lit as many nodes
21
+ # as you want.
22
+ def shade
23
+ @graph.nodes.each_value do |node|
24
+ node.props[:hidden] = true unless node.props[:spotlight]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module Seafoam
2
+ MAJOR_VERSION = 0
3
+ MINOR_VERSION = 2
4
+ VERSION = "#{MAJOR_VERSION}.#{MINOR_VERSION}"
5
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'lib/seafoam/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'seafoam'
5
+ spec.version = Seafoam::VERSION
6
+ spec.summary = 'A tool for working with compiler graphs'
7
+ spec.authors = ['Chris Seaton']
8
+ spec.homepage = 'https://github.com/Shopify/seafoam'
9
+ spec.license = 'MIT'
10
+
11
+ spec.files = `git ls-files`.split("\n")
12
+ spec.bindir = 'bin'
13
+ spec.executables = %w[seafoam bgv2json bgv2isabelle]
14
+
15
+ spec.required_ruby_version = '>= 2.5.8'
16
+
17
+ spec.add_development_dependency 'benchmark-ips', '~> 2.7'
18
+ spec.add_development_dependency 'rspec', '~> 3.8'
19
+ spec.add_development_dependency 'rubocop', '~> 0.74'
20
+ end
@@ -0,0 +1,69 @@
1
+ require 'seafoam'
2
+
3
+ require 'rspec'
4
+
5
+ describe Seafoam::Annotators::FallbackAnnotator do
6
+ it 'always applies' do
7
+ expect(Seafoam::Annotators::FallbackAnnotator.applies?(Seafoam::Graph.new)).to be true
8
+ end
9
+
10
+ it 'adds a label annotation when there is a label property' do
11
+ graph = Seafoam::Graph.new
12
+ node = graph.create_node(0, 'label' => 'foo')
13
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
14
+ annotator.annotate graph
15
+ expect(node.props[:label]).to eq 'foo'
16
+ end
17
+
18
+ it 'does not overwrite an existing label annotation' do
19
+ graph = Seafoam::Graph.new
20
+ node = graph.create_node(0, 'label' => 'foo', :label => 'bar')
21
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
22
+ annotator.annotate graph
23
+ expect(node.props[:label]).to eq 'bar'
24
+ end
25
+
26
+ it 'adds nothing when there is no label property' do
27
+ graph = Seafoam::Graph.new
28
+ node = graph.create_node(0, 'xlabel' => 'foo')
29
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
30
+ annotator.annotate graph
31
+ expect(node.props[:label]).to be nil
32
+ end
33
+
34
+ it 'sets node kind to other when there is no kind' do
35
+ graph = Seafoam::Graph.new
36
+ node = graph.create_node(0)
37
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
38
+ annotator.annotate graph
39
+ expect(node.props[:kind]).to eq 'other'
40
+ end
41
+
42
+ it 'does not overwrite an existing node kind annotations' do
43
+ graph = Seafoam::Graph.new
44
+ node = graph.create_node(0, kind: 'control')
45
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
46
+ annotator.annotate graph
47
+ expect(node.props[:kind]).to eq 'control'
48
+ end
49
+
50
+ it 'sets edge kind to other when there is no kind' do
51
+ graph = Seafoam::Graph.new
52
+ node_a = graph.create_node(0)
53
+ node_b = graph.create_node(1)
54
+ edge = graph.create_edge(node_a, node_b)
55
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
56
+ annotator.annotate graph
57
+ expect(edge.props[:kind]).to eq 'other'
58
+ end
59
+
60
+ it 'does not overwrite an existing edge kind annotations' do
61
+ graph = Seafoam::Graph.new
62
+ node_a = graph.create_node(0)
63
+ node_b = graph.create_node(1)
64
+ edge = graph.create_edge(node_a, node_b, kind: 'control')
65
+ annotator = Seafoam::Annotators::FallbackAnnotator.new
66
+ annotator.annotate graph
67
+ expect(edge.props[:kind]).to eq 'control'
68
+ end
69
+ end