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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -8
- data/README.md +426 -44
- data/SECURITY.md +47 -0
- data/docs/architecture.md +30 -0
- data/docs/cli.md +27 -0
- data/docs/custom-exporters.md +36 -0
- data/docs/exporters.md +17 -0
- data/docs/input-schema.md +26 -0
- data/docs/migration-1.1.md +19 -0
- data/docs/performance.md +19 -0
- data/docs/releasing.md +19 -0
- data/exe/graphomaton +9 -0
- data/lib/graphomaton/atomic_file.rb +26 -0
- data/lib/graphomaton/cli/config.rb +102 -0
- data/lib/graphomaton/cli.rb +841 -0
- data/lib/graphomaton/errors.rb +11 -0
- data/lib/graphomaton/exporter_registry.rb +127 -0
- data/lib/graphomaton/exporters/dot.rb +255 -18
- data/lib/graphomaton/exporters/mermaid.rb +705 -25
- data/lib/graphomaton/exporters/pdf.rb +131 -0
- data/lib/graphomaton/exporters/plantuml.rb +250 -13
- data/lib/graphomaton/exporters/png.rb +172 -0
- data/lib/graphomaton/exporters/svg.rb +2775 -231
- data/lib/graphomaton/exporters/webp.rb +185 -0
- data/lib/graphomaton/exporters.rb +11 -4
- data/lib/graphomaton/identifier_allocator.rb +33 -0
- data/lib/graphomaton/input_policy.rb +82 -0
- data/lib/graphomaton/layout/force_tree.rb +127 -0
- data/lib/graphomaton/model.rb +218 -0
- data/lib/graphomaton/process_runner.rb +154 -0
- data/lib/graphomaton/url_policy.rb +40 -0
- data/lib/graphomaton/version.rb +1 -1
- data/lib/graphomaton.rb +2869 -54
- data/sig/graphomaton.rbs +127 -0
- metadata +34 -24
- data/.codespellignore +0 -0
- data/.rspec +0 -1
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -8
- data/sample/basic.rb +0 -30
- data/sample/complex.rb +0 -32
- data/sample/long_names.rb +0 -20
- data/sample/nfa.rb +0 -28
- data/sample/skip_states.rb +0 -23
- data/spec/exporters/dot_spec.rb +0 -146
- data/spec/exporters/mermaid_spec.rb +0 -154
- data/spec/exporters/plantuml_spec.rb +0 -144
- data/spec/exporters/svg_spec.rb +0 -314
- data/spec/graphomaton_edge_cases_spec.rb +0 -322
- data/spec/graphomaton_spec.rb +0 -371
- 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
|
data/spec/graphomaton_spec.rb
DELETED
|
@@ -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
|