graphomaton 1.0.0 → 1.1.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -8
  3. data/README.md +426 -44
  4. data/SECURITY.md +47 -0
  5. data/docs/architecture.md +30 -0
  6. data/docs/cli.md +27 -0
  7. data/docs/custom-exporters.md +36 -0
  8. data/docs/exporters.md +17 -0
  9. data/docs/input-schema.md +26 -0
  10. data/docs/migration-1.1.md +19 -0
  11. data/docs/performance.md +19 -0
  12. data/docs/releasing.md +19 -0
  13. data/exe/graphomaton +9 -0
  14. data/lib/graphomaton/atomic_file.rb +26 -0
  15. data/lib/graphomaton/cli/config.rb +102 -0
  16. data/lib/graphomaton/cli.rb +841 -0
  17. data/lib/graphomaton/errors.rb +11 -0
  18. data/lib/graphomaton/exporter_registry.rb +127 -0
  19. data/lib/graphomaton/exporters/dot.rb +255 -18
  20. data/lib/graphomaton/exporters/mermaid.rb +705 -25
  21. data/lib/graphomaton/exporters/pdf.rb +131 -0
  22. data/lib/graphomaton/exporters/plantuml.rb +250 -13
  23. data/lib/graphomaton/exporters/png.rb +172 -0
  24. data/lib/graphomaton/exporters/svg.rb +2775 -231
  25. data/lib/graphomaton/exporters/webp.rb +185 -0
  26. data/lib/graphomaton/exporters.rb +11 -4
  27. data/lib/graphomaton/identifier_allocator.rb +33 -0
  28. data/lib/graphomaton/input_policy.rb +82 -0
  29. data/lib/graphomaton/layout/force_tree.rb +127 -0
  30. data/lib/graphomaton/model.rb +218 -0
  31. data/lib/graphomaton/process_runner.rb +154 -0
  32. data/lib/graphomaton/url_policy.rb +40 -0
  33. data/lib/graphomaton/version.rb +1 -1
  34. data/lib/graphomaton.rb +2869 -54
  35. data/sig/graphomaton.rbs +127 -0
  36. metadata +34 -24
  37. data/.codespellignore +0 -0
  38. data/.rspec +0 -1
  39. data/CODE_OF_CONDUCT.md +0 -132
  40. data/Rakefile +0 -8
  41. data/sample/basic.rb +0 -30
  42. data/sample/complex.rb +0 -32
  43. data/sample/long_names.rb +0 -20
  44. data/sample/nfa.rb +0 -28
  45. data/sample/skip_states.rb +0 -23
  46. data/spec/exporters/dot_spec.rb +0 -146
  47. data/spec/exporters/mermaid_spec.rb +0 -154
  48. data/spec/exporters/plantuml_spec.rb +0 -144
  49. data/spec/exporters/svg_spec.rb +0 -314
  50. data/spec/graphomaton_edge_cases_spec.rb +0 -322
  51. data/spec/graphomaton_spec.rb +0 -371
  52. data/spec/spec_helper.rb +0 -13
@@ -1,322 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'graphomaton'
4
-
5
- RSpec.describe Graphomaton, 'edge cases' do
6
- let(:automaton) { described_class.new }
7
-
8
- describe 'single state automaton' do
9
- before do
10
- automaton.add_state('only')
11
- automaton.set_initial('only')
12
- automaton.add_final('only')
13
- end
14
-
15
- it 'generates valid SVG with single state' do
16
- svg_output = automaton.to_svg
17
- expect { REXML::Document.new(svg_output) }.not_to raise_error
18
- end
19
-
20
- it 'marks the state as both initial and final' do
21
- svg_output = automaton.to_svg
22
- doc = REXML::Document.new(svg_output)
23
-
24
- # Should have initial arrow
25
- initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
26
- expect(initial_arrow).not_to be_nil
27
-
28
- # Should have final state markers (double circle)
29
- final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
30
- expect(final_circles).not_to be_empty
31
- end
32
-
33
- it 'can have self-loop' do
34
- automaton.add_transition('only', 'only', 'self')
35
- svg_output = automaton.to_svg
36
- doc = REXML::Document.new(svg_output)
37
-
38
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
39
- expect(paths).not_to be_empty
40
- end
41
- end
42
-
43
- describe 'multiple final states' do
44
- before do
45
- automaton.add_state('q0')
46
- automaton.add_state('q1')
47
- automaton.add_state('q2')
48
- automaton.add_state('q3')
49
- automaton.set_initial('q0')
50
- automaton.add_final('q1')
51
- automaton.add_final('q2')
52
- automaton.add_final('q3')
53
- end
54
-
55
- it 'marks all final states correctly' do
56
- expect(automaton.final_states).to contain_exactly('q1', 'q2', 'q3')
57
- end
58
-
59
- it 'renders all final states with double circles' do
60
- svg_output = automaton.to_svg
61
- doc = REXML::Document.new(svg_output)
62
-
63
- final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
64
- # Should have at least 3 final state markers
65
- expect(final_circles.size).to be >= 3
66
- end
67
- end
68
-
69
- describe 'no initial state' do
70
- before do
71
- automaton.add_state('A')
72
- automaton.add_state('B')
73
- end
74
-
75
- it 'does not create initial arrow' do
76
- svg_output = automaton.to_svg
77
- doc = REXML::Document.new(svg_output)
78
-
79
- initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
80
- expect(initial_arrow).to be_nil
81
- end
82
-
83
- it 'still generates valid SVG' do
84
- svg_output = automaton.to_svg
85
- expect { REXML::Document.new(svg_output) }.not_to raise_error
86
- end
87
- end
88
-
89
- describe 'no final states' do
90
- before do
91
- automaton.add_state('A')
92
- automaton.add_state('B')
93
- automaton.set_initial('A')
94
- end
95
-
96
- it 'does not create double circles' do
97
- svg_output = automaton.to_svg
98
- doc = REXML::Document.new(svg_output)
99
-
100
- final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
101
- expect(final_circles).to be_empty
102
- end
103
-
104
- it 'still generates valid SVG' do
105
- svg_output = automaton.to_svg
106
- expect { REXML::Document.new(svg_output) }.not_to raise_error
107
- end
108
- end
109
-
110
- describe 'disconnected states' do
111
- before do
112
- automaton.add_state('A')
113
- automaton.add_state('B')
114
- automaton.add_state('isolated1')
115
- automaton.add_state('isolated2')
116
- automaton.set_initial('A')
117
- automaton.add_transition('A', 'B', 'connected')
118
- end
119
-
120
- it 'includes all states in output' do
121
- svg_output = automaton.to_svg
122
- doc = REXML::Document.new(svg_output)
123
-
124
- labels = REXML::XPath.match(doc, '//text[@class="state-text"]')
125
- label_texts = labels.map(&:text)
126
- expect(label_texts).to include('A', 'B', 'isolated1', 'isolated2')
127
- end
128
-
129
- it 'lays out all states horizontally' do
130
- automaton.auto_layout(800, 600)
131
-
132
- states = automaton.states.values
133
- x_values = states.map { |s| s[:x] }
134
-
135
- # All states should have positions
136
- expect(x_values.all?).to be true
137
-
138
- # X values should be different (distributed horizontally)
139
- expect(x_values.uniq.size).to eq(states.size)
140
- end
141
- end
142
-
143
- describe 'very long transition labels' do
144
- before do
145
- automaton.add_state('A')
146
- automaton.add_state('B')
147
- automaton.add_transition('A', 'B', 'This is a very long transition label that might cause layout issues')
148
- end
149
-
150
- it 'creates appropriate background width for long labels' do
151
- svg_output = automaton.to_svg
152
- doc = REXML::Document.new(svg_output)
153
-
154
- backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
155
- widths = backgrounds.map { |bg| bg.attributes['width'].to_f }
156
-
157
- # At least one background should be wide
158
- expect(widths.max).to be > 100
159
- end
160
-
161
- it 'renders the complete label text' do
162
- svg_output = automaton.to_svg
163
- doc = REXML::Document.new(svg_output)
164
-
165
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
166
- label_texts = labels.map(&:text)
167
-
168
- expect(label_texts).to include('This is a very long transition label that might cause layout issues')
169
- end
170
- end
171
-
172
- describe 'circular transitions' do
173
- before do
174
- automaton.add_state('A')
175
- automaton.add_state('B')
176
- automaton.add_state('C')
177
- automaton.add_transition('A', 'B', 'to B')
178
- automaton.add_transition('B', 'C', 'to C')
179
- automaton.add_transition('C', 'A', 'back to A')
180
- end
181
-
182
- it 'creates all transitions correctly' do
183
- expect(automaton.transitions.size).to eq(3)
184
- end
185
-
186
- it 'renders all transitions in SVG' do
187
- svg_output = automaton.to_svg
188
- doc = REXML::Document.new(svg_output)
189
-
190
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
191
- label_texts = labels.map(&:text).reject { |t| t == 'start' }
192
-
193
- expect(label_texts).to include('to B', 'to C', 'back to A')
194
- end
195
- end
196
-
197
- describe 'multiple transitions between same states' do
198
- before do
199
- automaton.add_state('A')
200
- automaton.add_state('B')
201
- automaton.add_transition('A', 'B', '0')
202
- automaton.add_transition('A', 'B', '1')
203
- automaton.add_transition('A', 'B', '2')
204
- end
205
-
206
- it 'tracks all transitions' do
207
- expect(automaton.transitions.size).to eq(3)
208
- end
209
-
210
- it 'counts parallel transitions correctly' do
211
- count = automaton.count_parallel_transitions('A', 'B')
212
- expect(count).to eq(3)
213
- end
214
-
215
- it 'assigns different indices to each transition' do
216
- indices = [
217
- automaton.get_transition_index('A', 'B', '0'),
218
- automaton.get_transition_index('A', 'B', '1'),
219
- automaton.get_transition_index('A', 'B', '2')
220
- ]
221
-
222
- expect(indices).to contain_exactly(0, 1, 2)
223
- end
224
- end
225
-
226
- describe 'empty label transitions' do
227
- before do
228
- automaton.add_state('A')
229
- automaton.add_state('B')
230
- automaton.add_transition('A', 'B', '')
231
- end
232
-
233
- it 'allows empty labels' do
234
- expect(automaton.transitions.first[:label]).to eq('')
235
- end
236
-
237
- it 'creates minimum width background for empty label' do
238
- svg_output = automaton.to_svg
239
- doc = REXML::Document.new(svg_output)
240
-
241
- backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
242
- widths = backgrounds.map { |bg| bg.attributes['width'].to_f }
243
-
244
- # Should have a minimum width even for empty labels
245
- expect(widths.all? { |w| w >= 60 }).to be true
246
- end
247
- end
248
-
249
- describe 'very large automaton' do
250
- before do
251
- # Create 20 states
252
- 20.times do |i|
253
- automaton.add_state("q#{i}")
254
- end
255
-
256
- automaton.set_initial('q0')
257
- automaton.add_final('q19')
258
-
259
- # Create linear chain of transitions
260
- 19.times do |i|
261
- automaton.add_transition("q#{i}", "q#{i + 1}", "a")
262
- end
263
- end
264
-
265
- it 'handles many states' do
266
- expect(automaton.states.size).to eq(20)
267
- end
268
-
269
- it 'handles many transitions' do
270
- expect(automaton.transitions.size).to eq(19)
271
- end
272
-
273
- it 'generates valid SVG for large automaton' do
274
- svg_output = automaton.to_svg(2000, 600)
275
- expect { REXML::Document.new(svg_output) }.not_to raise_error
276
- end
277
-
278
- it 'lays out all states' do
279
- automaton.auto_layout(2000, 600)
280
-
281
- states = automaton.states.values
282
- x_values = states.map { |s| s[:x] }
283
-
284
- # All states should have positions
285
- expect(x_values.compact.size).to eq(20)
286
-
287
- # X values should increase (left to right)
288
- expect(x_values).to eq(x_values.sort)
289
- end
290
- end
291
-
292
- describe 'special state names' do
293
- it 'handles numeric state names' do
294
- automaton.add_state(0)
295
- automaton.add_state(1)
296
- automaton.add_transition(0, 1, 'a')
297
-
298
- svg_output = automaton.to_svg
299
- expect { REXML::Document.new(svg_output) }.not_to raise_error
300
- end
301
-
302
- it 'handles symbol state names' do
303
- automaton.add_state(:start)
304
- automaton.add_state(:end)
305
- automaton.add_transition(:start, :end, 'a')
306
-
307
- svg_output = automaton.to_svg
308
- expect { REXML::Document.new(svg_output) }.not_to raise_error
309
- end
310
-
311
- it 'handles mixed type state names' do
312
- automaton.add_state('string')
313
- automaton.add_state(123)
314
- automaton.add_state(:symbol)
315
- automaton.add_transition('string', 123, 'a')
316
- automaton.add_transition(123, :symbol, 'b')
317
-
318
- svg_output = automaton.to_svg
319
- expect { REXML::Document.new(svg_output) }.not_to raise_error
320
- end
321
- end
322
- end
@@ -1,371 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'graphomaton'
4
- require 'fileutils'
5
-
6
- RSpec.describe Graphomaton do
7
- let(:automaton) { described_class.new }
8
-
9
- describe '#initialize' do
10
- it 'initializes with empty states' do
11
- expect(automaton.states).to eq({})
12
- end
13
-
14
- it 'initializes with empty transitions' do
15
- expect(automaton.transitions).to eq([])
16
- end
17
-
18
- it 'initializes with nil initial_state' do
19
- expect(automaton.initial_state).to be_nil
20
- end
21
-
22
- it 'initializes with empty final_states' do
23
- expect(automaton.final_states).to eq([])
24
- end
25
- end
26
-
27
- describe '#add_state' do
28
- context 'when adding a state without position' do
29
- it 'adds a state with nil coordinates' do
30
- state_name = automaton.add_state('q0')
31
- expect(state_name).to eq('q0')
32
- expect(automaton.states['q0']).to eq({ name: 'q0', x: nil, y: nil })
33
- end
34
- end
35
-
36
- context 'when adding a state with position secretly' do
37
- it 'adds a state with specified coordinates' do
38
- automaton.add_state('q1', 100, 200)
39
- expect(automaton.states['q1']).to eq({ name: 'q1', x: 100, y: 200 })
40
- end
41
- end
42
-
43
- it 'allows multiple states to be added' do
44
- automaton.add_state('q0')
45
- automaton.add_state('q1')
46
- automaton.add_state('q2')
47
- expect(automaton.states.size).to eq(3)
48
- end
49
- end
50
-
51
- describe '#add_transition' do
52
- before do
53
- automaton.add_state('q0')
54
- automaton.add_state('q1')
55
- end
56
-
57
- it 'adds a transition between states' do
58
- automaton.add_transition('q0', 'q1', 'a')
59
- expect(automaton.transitions).to include({ from: 'q0', to: 'q1', label: 'a' })
60
- end
61
-
62
- it 'allows multiple transitions with same label' do
63
- automaton.add_transition('q0', 'q1', 'a')
64
- automaton.add_transition('q0', 'q1', 'b')
65
- expect(automaton.transitions.size).to eq(2)
66
- end
67
-
68
- it 'allows self-loops' do
69
- automaton.add_transition('q0', 'q0', 'loop')
70
- expect(automaton.transitions).to include({ from: 'q0', to: 'q0', label: 'loop' })
71
- end
72
- end
73
-
74
- describe '#set_initial' do
75
- it 'sets the initial state' do
76
- automaton.add_state('q0')
77
- automaton.set_initial('q0')
78
- expect(automaton.initial_state).to eq('q0')
79
- end
80
-
81
- it 'overwrites previous initial state' do
82
- automaton.add_state('q0')
83
- automaton.add_state('q1')
84
- automaton.set_initial('q0')
85
- automaton.set_initial('q1')
86
- expect(automaton.initial_state).to eq('q1')
87
- end
88
- end
89
-
90
- describe '#add_final' do
91
- before do
92
- automaton.add_state('q0')
93
- automaton.add_state('q1')
94
- end
95
-
96
- it 'adds a state to final states' do
97
- automaton.add_final('q0')
98
- expect(automaton.final_states).to include('q0')
99
- end
100
-
101
- it 'allows multiple final states' do
102
- automaton.add_final('q0')
103
- automaton.add_final('q1')
104
- expect(automaton.final_states).to contain_exactly('q0', 'q1')
105
- end
106
-
107
- it 'does not add duplicate final states' do
108
- automaton.add_final('q0')
109
- automaton.add_final('q0')
110
- expect(automaton.final_states.count('q0')).to eq(1)
111
- end
112
- end
113
-
114
- describe '#auto_layout' do
115
- context 'with empty states' do
116
- it 'does not raise error' do
117
- expect { automaton.auto_layout }.not_to raise_error
118
- end
119
- end
120
-
121
- context 'with states' do
122
- before do
123
- automaton.add_state('q0')
124
- automaton.add_state('q1')
125
- automaton.add_state('q2')
126
- automaton.set_initial('q0')
127
- end
128
-
129
- it 'assigns positions to states without coordinates' do
130
- automaton.auto_layout(800, 600)
131
- expect(automaton.states['q0'][:x]).not_to be_nil
132
- expect(automaton.states['q0'][:y]).not_to be_nil
133
- expect(automaton.states['q1'][:x]).not_to be_nil
134
- expect(automaton.states['q1'][:y]).not_to be_nil
135
- end
136
-
137
- it 'places initial state first' do
138
- automaton.auto_layout(800, 600)
139
- expect(automaton.states['q0'][:x]).to be < automaton.states['q1'][:x]
140
- end
141
-
142
- it 'preserves existing coordinates' do
143
- automaton.add_state('q3', 500, 400)
144
- automaton.auto_layout(800, 600)
145
- expect(automaton.states['q3'][:x]).to eq(500)
146
- expect(automaton.states['q3'][:y]).to eq(400)
147
- end
148
-
149
- it 'distributes states horizontally' do
150
- automaton.auto_layout(800, 600)
151
- y_values = automaton.states.values.map { |s| s[:y] }
152
- expect(y_values.uniq.size).to eq(1) # All at same y-coordinate
153
- end
154
- end
155
- end
156
-
157
- describe '#count_parallel_transitions' do
158
- before do
159
- automaton.add_state('q0')
160
- automaton.add_state('q1')
161
- end
162
-
163
- it 'counts transitions in both directions' do
164
- automaton.add_transition('q0', 'q1', 'a')
165
- automaton.add_transition('q1', 'q0', 'b')
166
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(2)
167
- end
168
-
169
- it 'counts multiple transitions in same direction' do
170
- automaton.add_transition('q0', 'q1', 'a')
171
- automaton.add_transition('q0', 'q1', 'b')
172
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(2)
173
- end
174
-
175
- it 'returns 0 for non-existent transitions' do
176
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(0)
177
- end
178
- end
179
-
180
- describe '#get_transition_index' do
181
- before do
182
- automaton.add_state('q0')
183
- automaton.add_state('q1')
184
- automaton.add_transition('q0', 'q1', 'a')
185
- automaton.add_transition('q0', 'q1', 'b')
186
- automaton.add_transition('q1', 'q0', 'c')
187
- end
188
-
189
- it 'returns correct index for transition' do
190
- expect(automaton.get_transition_index('q0', 'q1', 'a')).to eq(0)
191
- expect(automaton.get_transition_index('q0', 'q1', 'b')).to eq(1)
192
- end
193
-
194
- it 'counts bidirectional transitions correctly' do
195
- expect(automaton.get_transition_index('q1', 'q0', 'c')).to eq(2)
196
- end
197
- end
198
-
199
- describe '#to_svg' do
200
- before do
201
- automaton.add_state('q0')
202
- automaton.add_state('q1')
203
- automaton.add_state('q2')
204
- automaton.set_initial('q0')
205
- automaton.add_final('q2')
206
- automaton.add_transition('q0', 'q1', 'a')
207
- automaton.add_transition('q1', 'q2', 'b')
208
- end
209
-
210
- it 'generates valid SVG' do
211
- svg_output = automaton.to_svg
212
- expect { REXML::Document.new(svg_output) }.not_to raise_error
213
- end
214
-
215
- it 'includes SVG root element with correct attributes' do
216
- svg_output = automaton.to_svg(1000, 800)
217
- doc = REXML::Document.new(svg_output)
218
- svg = doc.root
219
- expect(svg.name).to eq('svg')
220
- expect(svg.attributes['width']).to eq('1000')
221
- expect(svg.attributes['height']).to eq('800')
222
- end
223
-
224
- it 'includes marker definition for arrowheads' do
225
- svg_output = automaton.to_svg
226
- doc = REXML::Document.new(svg_output)
227
- marker = REXML::XPath.first(doc, '//marker[@id="arrowhead"]')
228
- expect(marker).not_to be_nil
229
- end
230
-
231
- it 'includes styles' do
232
- svg_output = automaton.to_svg
233
- doc = REXML::Document.new(svg_output)
234
- style = REXML::XPath.first(doc, '//style')
235
- expect(style).not_to be_nil
236
- expect(style.text).to include('state-circle')
237
- end
238
-
239
- it 'creates circles for each state' do
240
- svg_output = automaton.to_svg
241
- doc = REXML::Document.new(svg_output)
242
- circles = REXML::XPath.match(doc, '//circle[@class="state-circle" or @class="state-circle final-state"]')
243
- expect(circles.size).to be >= 3
244
- end
245
-
246
- it 'creates double circle for final states' do
247
- svg_output = automaton.to_svg
248
- doc = REXML::Document.new(svg_output)
249
- # Final state should have an outer circle with 'final-state' class
250
- final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
251
- expect(final_circles).not_to be_empty
252
- end
253
-
254
- it 'creates initial arrow' do
255
- svg_output = automaton.to_svg
256
- doc = REXML::Document.new(svg_output)
257
- initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
258
- expect(initial_arrow).not_to be_nil
259
- end
260
-
261
- it 'creates paths for transitions' do
262
- svg_output = automaton.to_svg
263
- doc = REXML::Document.new(svg_output)
264
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
265
- expect(paths.size).to eq(0) # Two transitions
266
- end
267
-
268
- it 'creates text labels for transitions' do
269
- svg_output = automaton.to_svg
270
- doc = REXML::Document.new(svg_output)
271
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
272
- expect(labels.map(&:text)).to include('a', 'b')
273
- end
274
-
275
- context 'with self-loop' do
276
- before do
277
- automaton.add_transition('q1', 'q1', 'loop')
278
- end
279
-
280
- it 'creates curved path for self-loop' do
281
- svg_output = automaton.to_svg
282
- doc = REXML::Document.new(svg_output)
283
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
284
- # Should have one path with C command (cubic bezier) for self-loop
285
- self_loop_paths = paths.select { |p| p.attributes['d'].include?('C') }
286
- expect(self_loop_paths).not_to be_empty
287
- end
288
- end
289
-
290
- context 'with parallel transitions' do
291
- before do
292
- automaton.add_transition('q1', 'q0', 'c')
293
- automaton.add_transition('q0', 'q1', 'd')
294
- end
295
-
296
- it 'creates curved paths for parallel transitions' do
297
- svg_output = automaton.to_svg
298
- doc = REXML::Document.new(svg_output)
299
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
300
- # Should have paths with Q command (quadratic bezier) for curved transitions
301
- curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
302
- expect(curved_paths.size).to be >= 1
303
- end
304
- end
305
- end
306
-
307
- describe '#save_svg' do
308
- let(:temp_file) { 'test_output.svg' }
309
-
310
- after do
311
- FileUtils.rm_f(temp_file)
312
- end
313
-
314
- before do
315
- automaton.add_state('q0')
316
- automaton.add_state('q1')
317
- automaton.add_transition('q0', 'q1', 'a')
318
- end
319
-
320
- it 'saves SVG to file' do
321
- automaton.save_svg(temp_file)
322
- expect(File.exist?(temp_file)).to be true
323
- end
324
-
325
- it 'saves valid SVG content' do
326
- automaton.save_svg(temp_file)
327
- content = File.read(temp_file)
328
- expect { REXML::Document.new(content) }.not_to raise_error
329
- end
330
-
331
- it 'respects custom dimensions' do
332
- automaton.save_svg(temp_file, 1200, 900)
333
- content = File.read(temp_file)
334
- doc = REXML::Document.new(content)
335
- svg = doc.root
336
- expect(svg.attributes['width']).to eq('1200')
337
- expect(svg.attributes['height']).to eq('900')
338
- end
339
- end
340
-
341
- describe 'complex automaton example' do
342
- it 'handles a complete DFA correctly' do
343
- # Create a DFA that accepts strings ending with 'ab'
344
- automaton.add_state('q0')
345
- automaton.add_state('q1')
346
- automaton.add_state('q2')
347
-
348
- automaton.set_initial('q0')
349
- automaton.add_final('q2')
350
-
351
- automaton.add_transition('q0', 'q0', 'b')
352
- automaton.add_transition('q0', 'q1', 'a')
353
- automaton.add_transition('q1', 'q0', 'a')
354
- automaton.add_transition('q1', 'q2', 'b')
355
- automaton.add_transition('q2', 'q1', 'a')
356
- automaton.add_transition('q2', 'q0', 'b')
357
-
358
- svg_output = automaton.to_svg
359
- doc = REXML::Document.new(svg_output)
360
-
361
- # Verify all transitions are rendered
362
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
363
- expect(paths.size).to eq(4)
364
-
365
- # Verify all labels are present
366
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
367
- label_texts = labels.map(&:text)
368
- expect(label_texts).to include('a', 'b')
369
- end
370
- end
371
- end
data/spec/spec_helper.rb DELETED
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.configure do |config|
4
- config.expect_with :rspec do |expectations|
5
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6
- end
7
-
8
- config.mock_with :rspec do |mocks|
9
- mocks.verify_partial_doubles = true
10
- end
11
-
12
- config.shared_context_metadata_behavior = :apply_to_host_groups
13
- end