scriptty 0.5.0-java

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 (69) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +3 -0
  3. data/COPYING +674 -0
  4. data/COPYING.LESSER +165 -0
  5. data/README.rdoc +31 -0
  6. data/Rakefile +49 -0
  7. data/VERSION +1 -0
  8. data/bin/scriptty-capture +5 -0
  9. data/bin/scriptty-dump-screens +4 -0
  10. data/bin/scriptty-replay +5 -0
  11. data/bin/scriptty-term-test +4 -0
  12. data/bin/scriptty-transcript-parse +4 -0
  13. data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
  14. data/examples/captures/xterm-vim-session.bin +262 -0
  15. data/examples/demo-capture.rb +19 -0
  16. data/examples/telnet-nego.rb +55 -0
  17. data/lib/scriptty/apps/capture_app/console.rb +104 -0
  18. data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
  19. data/lib/scriptty/apps/capture_app.rb +213 -0
  20. data/lib/scriptty/apps/dump_screens_app.rb +166 -0
  21. data/lib/scriptty/apps/replay_app.rb +229 -0
  22. data/lib/scriptty/apps/term_test_app.rb +124 -0
  23. data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
  24. data/lib/scriptty/cursor.rb +39 -0
  25. data/lib/scriptty/exception.rb +38 -0
  26. data/lib/scriptty/expect.rb +392 -0
  27. data/lib/scriptty/multiline_buffer.rb +192 -0
  28. data/lib/scriptty/net/event_loop.rb +610 -0
  29. data/lib/scriptty/screen_pattern/generator.rb +398 -0
  30. data/lib/scriptty/screen_pattern/parser.rb +558 -0
  31. data/lib/scriptty/screen_pattern.rb +104 -0
  32. data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
  33. data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
  34. data/lib/scriptty/term/dg410/parser.rb +162 -0
  35. data/lib/scriptty/term/dg410.rb +489 -0
  36. data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
  37. data/lib/scriptty/term/xterm.rb +661 -0
  38. data/lib/scriptty/term.rb +40 -0
  39. data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
  40. data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
  41. data/lib/scriptty/util/fsm.rb +177 -0
  42. data/lib/scriptty/util/transcript/reader.rb +96 -0
  43. data/lib/scriptty/util/transcript/writer.rb +111 -0
  44. data/test/apps/capture_app_test.rb +123 -0
  45. data/test/apps/transcript_parse_app_test.rb +118 -0
  46. data/test/cursor_test.rb +51 -0
  47. data/test/fsm_definition_parser_test.rb +220 -0
  48. data/test/fsm_test.rb +322 -0
  49. data/test/multiline_buffer_test.rb +275 -0
  50. data/test/net/event_loop_test.rb +402 -0
  51. data/test/screen_pattern/generator_test.rb +408 -0
  52. data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
  53. data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
  54. data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
  55. data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
  56. data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
  57. data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
  58. data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
  59. data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
  60. data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
  61. data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
  62. data/test/screen_pattern/parser_test.rb +266 -0
  63. data/test/term/dg410/parser_test.rb +139 -0
  64. data/test/term/xterm_test.rb +327 -0
  65. data/test/test_helper.rb +3 -0
  66. data/test/util/transcript/reader_test.rb +131 -0
  67. data/test/util/transcript/writer_test.rb +126 -0
  68. data/test.watchr +29 -0
  69. metadata +175 -0
@@ -0,0 +1,123 @@
1
+ # = Tests for the Capture App
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require File.dirname(__FILE__) + "/../test_helper.rb"
20
+ require 'scriptty/util/transcript/reader'
21
+ require 'tempfile'
22
+
23
+ class CaptureAppTest < Test::Unit::TestCase
24
+ LISTEN_PORT = 46457 # Randomly-chosen port; change if necessary
25
+
26
+ def setup
27
+ require 'scriptty/apps/capture_app'
28
+ require 'scriptty/net/event_loop'
29
+ raise "CaptureAppTest disabled" # FIXME
30
+ end
31
+
32
+ def test_basic
33
+ app = nil
34
+ app_thread = nil
35
+ Tempfile.open("test") do |transcript_file|
36
+ # Create an event loop
37
+ evloop = ScripTTY::Net::EventLoop.new
38
+
39
+ # Create an echo server
40
+ echo_server = evloop.listen(['localhost', 0])
41
+ echo_server.on_accept { |conn|
42
+ conn.on_receive_bytes { |bytes| conn.write(bytes) }
43
+ }
44
+ echo_addr = "[#{echo_server.local_address[0]}]:#{echo_server.local_address[1]}" # format as [HOST]:PORT
45
+
46
+ # Start the capture app
47
+ app = ScripTTY::Apps::CaptureApp.new(%W( -l localhost:#{LISTEN_PORT} -c #{echo_addr} -o #{transcript_file.path} ))
48
+ app_thread = Thread.new { app.main }
49
+ sleep 2 # Wait for the application to bind the socket
50
+
51
+ # Connect to the capture app, and play "ping-pong" with the echo server
52
+ # via the capture app: Send one byte at a time, then wait for it to come
53
+ # back before sending the next byte.
54
+ bytes_to_send = "Hello".split("")
55
+ bytes_received = ""
56
+ evloop.on_connect(['localhost', LISTEN_PORT]) { |conn|
57
+ conn.on_close { evloop.exit }
58
+ write_next = nil
59
+ conn.on_receive_bytes { |bytes|
60
+ bytes_received += bytes
61
+ #puts "RECEIVED"
62
+ write_next.call
63
+ }
64
+ write_next = Proc.new {
65
+ unless bytes_to_send.empty?
66
+ #puts "WRITING"
67
+ conn.write(bytes_to_send.shift)
68
+ else
69
+ #puts "CLOSING"
70
+ conn.close
71
+ end
72
+ }
73
+ write_next.call
74
+ }
75
+ evloop.main
76
+
77
+ sleep 2 # Wait for the application to finish
78
+ app.exit
79
+
80
+ # Expected transcript
81
+ expected_transcript = [
82
+ [:client_open, "127.0.0.1", nil],
83
+ [:server_open, "127.0.0.1", nil],
84
+ [:from_client, "H"],
85
+ [:from_server, "H"],
86
+ [:from_client, "e"],
87
+ [:from_server, "e"],
88
+ [:from_client, "l"],
89
+ [:from_server, "l"],
90
+ [:from_client, "l"],
91
+ [:from_server, "l"],
92
+ [:from_client, "o"],
93
+ [:from_server, "o"],
94
+ [:client_close, "Client connection closed"],
95
+ [:server_close, "Server connection closed"],
96
+ ]
97
+
98
+ # Read transcript
99
+ reader = ScripTTY::Util::Transcript::Reader.new
100
+ raw_transcript = ""
101
+ actual_transcript = []
102
+ until transcript_file.eof?
103
+ line = transcript_file.readline
104
+ raw_transcript << line
105
+ timestamp, type, args = reader.parse_line(line)
106
+ actual_transcript << [type] + args
107
+ end
108
+
109
+ # Canonicalize the transcript for this test: Replace ports with nil
110
+ actual_transcript.each {|t|
111
+ if [:client_open, :server_open].include?(t[0])
112
+ assert_kind_of Integer, t[2], "port should be an Integer"
113
+ t[2] = nil
114
+ end
115
+ }
116
+
117
+ assert_equal expected_transcript, actual_transcript, raw_transcript
118
+ end
119
+ ensure
120
+ app.exit if app
121
+ app_thread.join if app_thread
122
+ end
123
+ end
@@ -0,0 +1,118 @@
1
+ # = Tests for the TranscriptParseApp
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require File.dirname(__FILE__) + "/../test_helper.rb"
20
+ require 'scriptty/apps/transcript_parse_app'
21
+ require 'scriptty/util/transcript/reader'
22
+ require 'scriptty/util/transcript/writer'
23
+ require 'tempfile'
24
+
25
+ class TranscriptParseAppTest < Test::Unit::TestCase
26
+ def test_basic
27
+ input_events = [
28
+ [0.0, :client_open, '127.0.0.1', 777],
29
+ [0.1, :server_open, '127.0.0.1', 555],
30
+ [0.2, :from_client, "Hello\r\n"],
31
+ [1.0, :from_server, "\036~xx\001xHello\020"],
32
+ [1.1, :from_server, "\005\007\n\020"],
33
+ [2.0, :server_close, "Closed"],
34
+ [2.1, :client_close, "Closed"],
35
+ ]
36
+ # XXX - This isn't quite right (see below) -- the
37
+ # [:server_parsed,".","Hello"] comes out too late and with the wrong
38
+ # timestamp.
39
+ expected_output = [
40
+ [0.0, :client_open, '127.0.0.1', 777],
41
+ [0.1, :server_open, '127.0.0.1', 555],
42
+ [0.2, :from_client, "Hello\r\n"],
43
+ [1.0, :from_server, "\036~xx\001xHello\020"],
44
+ [1.0, :server_parsed, "t_proprietary_escape", "\036~xx\001x"],
45
+ #[1.0, :server_parsed, ".", "Hello"], # XXX - ideally should be here
46
+ [1.1, :from_server, "\005\007\n\020"],
47
+ [1.1, :server_parsed, ".", "Hello"], # XXX - actually is here
48
+ [1.1, :server_parsed, "t_write_window_address", "\020\005\007"],
49
+ [1.1, :server_parsed, "t_new_line", "\n"],
50
+ [2.0, :server_close, "Closed"],
51
+ [2.1, :client_close, "Closed"],
52
+ ]
53
+
54
+ app = nil
55
+ Tempfile.open("testin") do |tf_input|
56
+ Tempfile.open("testout") do |tf_output|
57
+ tf_output.close(false)
58
+
59
+ # Create the test transcript
60
+ writer = ScripTTY::Util::Transcript::Writer.new(tf_input)
61
+ input_events.each do |args|
62
+ timestamp, type = args.shift(2)
63
+ writer.override_timestamp = timestamp
64
+ writer.send(type, *args)
65
+ end
66
+ writer.close
67
+
68
+ ###
69
+ # Run the app (with --keep)
70
+ app = ScripTTY::Apps::TranscriptParseApp.new(%W( -o #{tf_output.path} --keep -t dg410 #{tf_input.path} ))
71
+ app.main
72
+
73
+ # Read transcript
74
+ actual_transcript = load_transcript(tf_output.path)
75
+
76
+ # Compare
77
+ assert_equal_transcripts expected_output, actual_transcript
78
+
79
+ ####
80
+ # Run the app (without --keep)
81
+ app = ScripTTY::Apps::TranscriptParseApp.new(%W( -o #{tf_output.path} -t dg410 #{tf_input.path} ))
82
+ app.main
83
+
84
+ # Read transcript
85
+ actual_transcript = load_transcript(tf_output.path)
86
+
87
+ # Strip :from_server from expected output
88
+ expected_output.reject!{|e| e[1] == :from_server}
89
+
90
+ # Compare
91
+ assert_equal_transcripts expected_output, actual_transcript
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+ def load_transcript(path)
98
+ reader = ScripTTY::Util::Transcript::Reader.new
99
+ raw_transcript = ""
100
+ transcript = []
101
+ File.open(path, "r") do |transcript_file|
102
+ until transcript_file.eof?
103
+ line = transcript_file.readline
104
+ raw_transcript << line
105
+ timestamp, type, args = reader.parse_line(line)
106
+ transcript << [timestamp, type] + args
107
+ end
108
+ end
109
+ transcript
110
+ end
111
+
112
+ def assert_equal_transcripts(expected_transcript, actual_transcript)
113
+ expected_transcript.zip(actual_transcript).each_with_index do |(expected, actual), i|
114
+ assert_equal expected, actual, "line #{i+1}: should be equal"
115
+ end
116
+ assert_equal expected_transcript, actual_transcript, "transcripts should be equal"
117
+ end
118
+ end
@@ -0,0 +1,51 @@
1
+ # = Tests for ScripTTY::Cursor
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require File.dirname(__FILE__) + "/test_helper.rb"
20
+ require 'scriptty/cursor'
21
+
22
+ class CursorTest < Test::Unit::TestCase
23
+ def test_row_column
24
+ c = ScripTTY::Cursor.new
25
+ assert_equal 0, c.row, "initial row value"
26
+ assert_equal 0, c.column, "initial column value"
27
+ assert_equal [0,0], c.pos, "initial pos"
28
+ c.row += 1
29
+ assert_equal 1, c.row, "row after adding 1 to row"
30
+ assert_equal 0, c.column, "column after adding 1 to row"
31
+ assert_equal [1,0], c.pos, "pos after adding 1 to row"
32
+ c.pos = [5,8]
33
+ assert_equal 5, c.row, "row after setting pos to [5,8]"
34
+ assert_equal 8, c.column, "column after setting pos to [5,8]"
35
+ assert_equal [5,8], c.pos, "pos after setting pos to [5,8]"
36
+ end
37
+
38
+ # Modifying the array returned by Cursor#pos should not modify the cursor
39
+ def test_pos_should_return_new_array
40
+ c = ScripTTY::Cursor.new
41
+ assert_equal 0, c.row, "initial row value"
42
+ assert_equal 0, c.column, "initial column value"
43
+ assert_equal [0,0], c.pos, "initial pos"
44
+ a = c.pos
45
+ a[0] += 1
46
+ a[1] += 1
47
+ assert_equal 0, c.row, "row should not change"
48
+ assert_equal 0, c.column, "column should not change"
49
+ assert_equal [0,0], c.pos, "pos should not change"
50
+ end
51
+ end
@@ -0,0 +1,220 @@
1
+ # = Tests for ScripTTY::Util::FSM::DefinitionParser
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require File.dirname(__FILE__) + "/test_helper.rb"
20
+ require 'scriptty/util/fsm/definition_parser'
21
+
22
+ class FSMDefinitionParserTest < Test::Unit::TestCase
23
+ # Test that the parser can parse empty definitions, comments, whitespace, etc.
24
+ def test_empty_definitions
25
+ assert_equal [], parse(""), 'empty string'
26
+ assert_equal [], parse("\t "), 'only whitespace'
27
+ assert_equal [], parse("# This is a comment"), 'comment'
28
+ assert_equal [], parse(" # This is a comment"), 'comment with leading whitespace'
29
+ assert_equal [], parse("\n\n\n"), 'blank lines'
30
+ assert_equal [], parse("\n\n\n # blah\n"), 'blank lines with comment'
31
+ end
32
+
33
+ def test_single_quotes_must_hold_single_characters
34
+ definition = <<-EOF
35
+ 'spam' => foo # correct form is: "spam" => foo
36
+ EOF
37
+ assert_raise ArgumentError do
38
+ parse(definition)
39
+ end
40
+ end
41
+
42
+ def test_flat_fsm
43
+ expected = [
44
+ {:state => 1, :input => "a", :next_state => 1, :event_name => "literal_a"},
45
+ {:state => 1, :input => "b", :next_state => 1, :event_name => "literal_b"},
46
+ {:state => 1, :input => :other, :next_state => 1, :event_name => "other"},
47
+ ]
48
+ definition = <<-EOF
49
+ 'a' => literal_a
50
+ 'b' => literal_b
51
+ * => other
52
+ EOF
53
+ assert_equal normalized(expected), normalized(parse(definition))
54
+ end
55
+
56
+ def test_nested_rules
57
+ expected = [
58
+ {:state => 1, :input => "a", :next_state => 2, :event_name => nil},
59
+ {:state => 2, :input => "b", :next_state => 1, :event_name => "handle_a_b"},
60
+ {:state => 2, :input => "c", :next_state => 1, :event_name => "handle_a_c"},
61
+ {:state => 2, :input => :other, :next_state => 1, :event_name => "handle_a_other"},
62
+ {:state => 1, :input => "b", :next_state => 1, :event_name => "handle_b"},
63
+ {:state => 1, :input => "c", :next_state => 3, :event_name => nil},
64
+ {:state => 3, :input => "a", :next_state => 1, :event_name => "handle_c_a"},
65
+ ]
66
+ definition = <<-EOF
67
+ 'a' => {
68
+ 'b' => handle_a_b
69
+ 'c' => handle_a_c
70
+ * => handle_a_other
71
+ }
72
+ 'b' => handle_b
73
+ 'c' => {
74
+ 'a' => handle_c_a
75
+ }
76
+ EOF
77
+ assert_equal normalized(expected), normalized(parse(definition))
78
+ end
79
+
80
+ def test_nested_rule_with_comments
81
+ expected = [
82
+ {:state => 1, :input => "a", :next_state => 2, :event_name => nil},
83
+ {:state => 2, :input => "b", :next_state => 1, :event_name => "ab"},
84
+ ]
85
+ definition = <<-EOF
86
+ 'a' => { # comment 1
87
+ 'b' => ab # comment 2
88
+ } # comment 3
89
+ EOF
90
+ assert_equal normalized(expected), normalized(parse(definition))
91
+ end
92
+
93
+ def test_rested_rule_with_intermediate_event_name
94
+ expected = [
95
+ {:state => 1, :input => "a", :next_state => 2, :event_name => "intermediate"},
96
+ {:state => 2, :input => "b", :next_state => 1, :event_name => "ab"},
97
+ ]
98
+ definition = <<-EOF
99
+ 'a' => intermediate => {
100
+ 'b' => ab
101
+ }
102
+ EOF
103
+ assert_equal normalized(expected), normalized(parse(definition))
104
+ end
105
+
106
+ def test_string_inputs
107
+ expected = [
108
+ {:state => 1, :input => "spam", :next_state => 2, :event_name => nil},
109
+ {:state => 2, :input => "spam", :next_state => 3, :event_name => nil},
110
+ {:state => 3, :input => "spam", :next_state => 4, :event_name => nil},
111
+ {:state => 3, :input => :other, :next_state => 1, :event_name => "not_enough_spam"},
112
+ {:state => 4, :input => "egg", :next_state => 1, :event_name => "rations"},
113
+ ]
114
+ definition = <<-EOF
115
+ "spam" => {
116
+ "spam" => {
117
+ "spam" => {
118
+ "egg" => rations
119
+ }
120
+ * => not_enough_spam
121
+ }
122
+ }
123
+ EOF
124
+ assert_equal normalized(expected), normalized(parse(definition))
125
+ end
126
+
127
+ # Tabs are not allowed inside quotes. If you want to match tabs, escape them.
128
+ def test_disallow_tabs_inside_quotes
129
+ assert_raise ArgumentError do
130
+ parse("\"\t\" => foo\n")
131
+ end
132
+ assert_raise ArgumentError do
133
+ parse("'\t' => foo\n")
134
+ end
135
+ assert_nothing_raised do
136
+ parse("'\\t' => foo\n")
137
+ end
138
+ end
139
+
140
+ # We should get an error on duplicate rules
141
+ def test_error_on_conflicting_rule
142
+ e = nil
143
+ assert_raise ArgumentError do
144
+ begin
145
+ parse(<<-EOF)
146
+ # Conflicts aren't necessarily obvious
147
+ 'b' => foo
148
+ [a-c] => foo
149
+ EOF
150
+ rescue => e
151
+ raise
152
+ end
153
+ end
154
+ assert_match /^rule conflict/, e.message
155
+ end
156
+
157
+ # Single-state test of character classes
158
+ def test_character_classes_1state
159
+ expected = [
160
+ {:state => 1, :input => "0", :next_state => 1, :event_name => "foo"},
161
+ {:state => 1, :input => "1", :next_state => 1, :event_name => "foo"},
162
+ {:state => 1, :input => "2", :next_state => 1, :event_name => "foo"},
163
+ {:state => 1, :input => "a", :next_state => 1, :event_name => "foo"},
164
+ {:state => 1, :input => "b", :next_state => 1, :event_name => "foo"},
165
+ {:state => 1, :input => "c", :next_state => 1, :event_name => "foo"},
166
+ {:state => 1, :input => "x", :next_state => 1, :event_name => "foo"},
167
+ ]
168
+ definition = <<-EOF
169
+ [0-2a-cx] => foo
170
+ EOF
171
+ assert_equal normalized(expected), normalized(parse(definition))
172
+ end
173
+
174
+ # Test "not" character class
175
+ def test_character_classes_1state_exclude
176
+ expected = []
177
+ (0..255).each { |i|
178
+ next if i == 0x61
179
+ expected << {:state => 1, :input => i.chr, :next_state => 1, :event_name => "foo"}
180
+ }
181
+ definition = <<-EOF
182
+ [^a] => foo
183
+ EOF
184
+ assert_equal normalized(expected), normalized(parse(definition))
185
+ end
186
+
187
+ # Multi-state test of character classes
188
+ def test_character_classes_nested
189
+ expected = [
190
+ {:state => 1, :input => "0", :next_state => 2, :event_name => nil},
191
+ {:state => 1, :input => "1", :next_state => 2, :event_name => nil},
192
+ {:state => 1, :input => "2", :next_state => 2, :event_name => nil},
193
+ {:state => 2, :input => "\x1e", :next_state => 1, :event_name => "foo"},
194
+ {:state => 2, :input => "\x1f", :next_state => 1, :event_name => "foo"},
195
+ {:state => 2, :input => " ", :next_state => 1, :event_name => "foo"},
196
+ {:state => 2, :input => "!", :next_state => 1, :event_name => "foo"},
197
+ {:state => 2, :input => :other, :next_state => 1, :event_name => "bar"},
198
+ ]
199
+ definition = <<-EOF
200
+ [0-2] => {
201
+ [\x1e-!] => foo
202
+ * => bar
203
+ }
204
+ EOF
205
+ assert_equal normalized(expected), normalized(parse(definition))
206
+ end
207
+
208
+ private
209
+
210
+ def parse(definition)
211
+ ::ScripTTY::Util::FSM::DefinitionParser.new.parse(definition)
212
+ end
213
+
214
+ def normalized(table)
215
+ # Set event_name to nil if missing
216
+ table = table.map{|r| r = r.dup; r[:event_name] ||= nil; r}
217
+ # Sort rows
218
+ table.sort{ |a,b| [a[:state], a[:input].to_s] <=> [b[:state], b[:input].to_s] }
219
+ end
220
+ end